Merge remote-tracking branch 'goog/nyc-mr1-dev-plus-aosp' into HEAD
diff --git a/library/common-full-support.mk b/library/common-full-support.mk
index 9ff82ce..43a6433 100644
--- a/library/common-full-support.mk
+++ b/library/common-full-support.mk
@@ -9,7 +9,7 @@
# LOCAL_RESOURCE_DIR := \
# $(LOCAL_PATH)/res
#
-# include frameworks/opt/setupwizard/library/common-eclair-mr1.mk
+# include frameworks/opt/setupwizard/library/common-full-support.mk
#
# Check that LOCAL_RESOURCE_DIR is defined
diff --git a/library/eclair-mr1/res/layout/suw_items_switch.xml b/library/eclair-mr1/res/layout/suw_items_switch.xml
index 14c7650..62bb479 100644
--- a/library/eclair-mr1/res/layout/suw_items_switch.xml
+++ b/library/eclair-mr1/res/layout/suw_items_switch.xml
@@ -45,7 +45,7 @@
android:layout_weight="1"
android:orientation="vertical">
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_title"
style="@style/SuwItemTitle.Verbose"
android:layout_width="match_parent"
@@ -54,7 +54,7 @@
android:textAlignment="viewStart"
tools:ignore="UnusedAttribute" />
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_summary"
style="@style/SuwItemSummary"
android:layout_width="match_parent"
diff --git a/library/eclair-mr1/res/values/styles.xml b/library/eclair-mr1/res/values/styles.xml
index fa954c6..25e7757 100644
--- a/library/eclair-mr1/res/values/styles.xml
+++ b/library/eclair-mr1/res/values/styles.xml
@@ -36,6 +36,7 @@
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwCardBackground">@drawable/suw_card_bg_dark</item>
+ <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
<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>
@@ -59,30 +60,33 @@
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwCardBackground">@drawable/suw_card_bg_light</item>
+ <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
<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.SuwItemSummary</item>
</style>
- <!-- Placeholder for GLIF dark theme, colors are not updated yet -->
<style name="SuwThemeGlif" parent="Theme.AppCompat.NoActionBar">
<item name="android:indeterminateTint" tools:ignore="NewApi">?attr/colorControlActivated</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode" tools:ignore="NewApi">src_in</item>
<item name="android:listPreferredItemHeight">@dimen/suw_items_preferred_height</item>
+ <item name="android:listPreferredItemPaddingEnd" tools:ignore="NewApi">?attr/suwMarginSides</item>
+ <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:textColorLink">@color/suw_link_color_dark</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="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
<item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
<item name="textAppearanceListItem">@style/TextAppearance.SuwGlifItemTitle</item>
@@ -104,10 +108,11 @@
<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="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
<item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
<item name="textAppearanceListItem">@style/TextAppearance.SuwGlifItemTitle</item>
diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java b/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java
index aaeaf34..40e6bd0 100644
--- a/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java
+++ b/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java
@@ -82,6 +82,7 @@
public void onBindView(View view) {
super.onBindView(view);
final SwitchCompat switchView = (SwitchCompat) view.findViewById(R.id.suw_items_switch);
+ switchView.setOnCheckedChangeListener(null);
switchView.setChecked(mChecked);
switchView.setOnCheckedChangeListener(this);
switchView.setEnabled(isEnabled());
@@ -93,6 +94,7 @@
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mChecked = 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
index 2c53ee7..e6fa497 100644
--- a/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
+++ b/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
@@ -118,13 +118,11 @@
info.setFocusable(true);
info.setClickable(true);
getBoundsForSpan(span, mTempRect);
- if (!mTempRect.isEmpty()) {
- info.setBoundsInParent(getBoundsForSpan(span, mTempRect));
- } else {
+ if (mTempRect.isEmpty()) {
Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
mTempRect.set(0, 0, 1, 1);
- info.setBoundsInParent(mTempRect);
}
+ info.setBoundsInParent(mTempRect);
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
}
@@ -171,22 +169,36 @@
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
+ if (layout != null) {
+ Spanned spannedText = (Spanned) text;
+ final int spanStart = spannedText.getSpanStart(span);
+ final int spanEnd = spannedText.getSpanEnd(span);
+ 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);
+ if (lineEnd == lineStart) {
+ // If the span is on a single line, adjust both the left and right bounds
+ // so outrect is exactly bounding the span.
+ outRect.left = (int) Math.min(xStart, xEnd);
+ outRect.right = (int) Math.max(xStart, xEnd);
+ } else {
+ // If the span wraps across multiple lines, only use the first line (as returned
+ // by layout.getLineBounds above), and adjust the "start" of outrect to where
+ // the span starts, leaving the "end" of outrect at the end of the line.
+ // ("start" being left for LTR, and right for RTL)
+ if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) {
+ outRect.right = (int) xStart;
+ } else {
+ outRect.left = (int) xStart;
+ }
+ }
- // Offset for padding
- outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
+ // Offset for padding
+ outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
+ }
}
return outRect;
}
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 @@
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 @@
@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 @@
@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 @@
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
index d79d149..2931e27 100644
--- a/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java
+++ b/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java
@@ -22,6 +22,7 @@
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
import android.util.Log;
@@ -99,13 +100,40 @@
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);
+ // Set text first before doing anything else because setMovementMethod internally calls
+ // setText. This in turn ends up calling this method with mText as the first parameter
super.setText(text, type);
+ boolean hasLinks = hasLinks(text);
+
+ if (hasLinks) {
+ // When a TextView has a movement method, it will set the view to clickable. This makes
+ // View.onTouchEvent always return true and consumes the touch event, essentially
+ // nullifying any return values of MovementMethod.onTouchEvent.
+ // To still allow propagating touch events to the parent when this view doesn't have
+ // links, we only set the movement method here if the text contains links.
+ setMovementMethod(LinkMovementMethod.getInstance());
+ } else {
+ setMovementMethod(null);
+ }
+ // ExploreByTouchHelper automatically enables focus for RichTextView
+ // even though it may not have any links. Causes problems during talkback
+ // as individual TextViews consume touch events and thereby reducing the focus window
+ // shown by Talkback. Disable focus if there are no links
+ setFocusable(hasLinks);
+ }
+
+ private boolean hasLinks(CharSequence text) {
+ if (text instanceof Spanned) {
+ final ClickableSpan[] spans =
+ ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class);
+ return spans.length > 0;
+ }
+ return false;
}
@Override
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
index 8d42fa3..76885b9 100644
--- a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java
+++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java
@@ -121,6 +121,53 @@
info.recycle();
}
+ @SmallTest
+ public void testNullLayout() {
+ // Setting the padding will cause the layout to be null-ed out.
+ mTextView.setPadding(1, 1, 1, 1);
+
+ AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mHelper.onPopulateNodeForVirtualView(0, info);
+
+ Rect bounds = new Rect();
+ info.getBoundsInParent(bounds);
+ assertEquals("LinkSpan bounds should be (0, 0, 1, 1)",
+ new Rect(0, 0, 1, 1), bounds);
+
+ info.recycle();
+ }
+
+ @SmallTest
+ public void testRtlLayout() {
+ // Redo setUp with a Hebrew (RTL) string.
+ mSpan = new LinkSpan("foobar");
+ SpannableStringBuilder ssb = new SpannableStringBuilder("מכונה בתרגום");
+ 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));
+ // End redo setup
+
+ AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mHelper.onPopulateNodeForVirtualView(1, info);
+
+ assertEquals("LinkSpan description should be \"כ\"",
+ "כ", 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 (70.5dp, 0dp, 78.5dp, 20.5dp)",
+ new Rect(dp2Px(70.5f), dp2Px(0f), dp2Px(78.5f), dp2Px(20.5f)), bounds);
+
+ info.recycle();
+ }
+
private int dp2Px(float dp) {
if (mDisplayMetrics == null) {
mDisplayMetrics = getContext().getResources().getDisplayMetrics();
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
index c591580..b5bf73e 100644
--- a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java
+++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java
@@ -72,4 +72,43 @@
assertTrue("The span should be a TextAppearanceSpan",
spans[0] instanceof TextAppearanceSpan);
}
+
+ @SmallTest
+ public void testTextContaininingLinksAreFocusable() {
+ Annotation testLink = new Annotation("link", "value");
+ SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("Linked");
+ spannableStringBuilder.setSpan(testLink, 0, 3, 0);
+
+ RichTextView view = new RichTextView(getContext());
+ view.setText(spannableStringBuilder);
+
+ assertTrue("TextView should be focusable since it contains spans", view.isFocusable());
+ }
+
+
+ @SmallTest
+ public void testTextContainingNoLinksAreNotFocusable() {
+ RichTextView textView = new RichTextView(getContext());
+ textView.setText("Thou shall not be focusable!");
+
+ assertFalse("TextView should not be focusable since it does not contain any span",
+ textView.isFocusable());
+ }
+
+
+ // Based on the text contents of the text view, the "focusable" property of the element
+ // should also be automatically changed.
+ @SmallTest
+ public void testRichTxtViewFocusChangesWithTextChange() {
+ RichTextView textView = new RichTextView(getContext());
+ textView.setText("Thou shall not be focusable!");
+
+ assertFalse(textView.isFocusable());
+
+ SpannableStringBuilder spannableStringBuilder =
+ new SpannableStringBuilder("I am focusable");
+ spannableStringBuilder.setSpan(new Annotation("link", "focus:on_me"), 0, 1, 0);
+ textView.setText(spannableStringBuilder);
+ assertTrue(textView.isFocusable());
+ }
}
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
index e7c93ba..479516f 100644
--- a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java
+++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java
@@ -87,6 +87,37 @@
}
@SmallTest
+ public void testRebind() {
+ SwitchItem item1 = new SwitchItem();
+ item1.setTitle("TestTitle1");
+ item1.setSummary("TestSummary1");
+ item1.setChecked(false);
+
+ SwitchItem item2 = new SwitchItem();
+ item2.setTitle("TestTitle2");
+ item2.setSummary("TestSummary2");
+ item2.setChecked(true);
+
+ View view = createLayout();
+
+ item1.onBindView(view);
+ item2.onBindView(view);
+
+ // Switch should be bound to item2, and therefore checked
+ assertTrue("Switch should be checked", mSwitch.isChecked());
+
+ // Switching the switch to false should change the checked state of item 2 only
+ mSwitch.setChecked(false);
+ assertFalse("Item1 should still be unchecked", item1.isChecked());
+ assertFalse("Item2 should not be checked", item2.isChecked());
+
+ // Switching the switch to true should change the checked state of item 2 only
+ mSwitch.setChecked(true);
+ assertFalse("Item1 should still be unchecked", item1.isChecked());
+ assertTrue("Item2 should be checked", item2.isChecked());
+ }
+
+ @SmallTest
public void testListenerSetChecked() {
// Check that calling setChecked on the item will also call the listener.
diff --git a/library/full-support/res/layout/suw_glif_recycler_template_card.xml b/library/full-support/res/layout/suw_glif_recycler_template_card.xml
index cf67bd9..7b5c6b0 100644
--- a/library/full-support/res/layout/suw_glif_recycler_template_card.xml
+++ b/library/full-support/res/layout/suw_glif_recycler_template_card.xml
@@ -15,21 +15,35 @@
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suw_pattern_bg"
style="@style/SuwGlifCardBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fitsSystemWindows="true">
+ android:fitsSystemWindows="true"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
- <FrameLayout
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+ <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
style="@style/SuwGlifCardContainer"
android:layout_width="@dimen/suw_glif_card_width"
- android:layout_height="@dimen/suw_glif_card_height"
- android:layout_centerInParent="true">
+ android:layout_height="wrap_content"
+ android:height="@dimen/suw_glif_card_height">
<include layout="@layout/suw_glif_recycler_template_content" />
- </FrameLayout>
+ </com.android.setupwizardlib.view.IntrinsicSizeFrameLayout>
-</RelativeLayout>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+</LinearLayout>
diff --git a/library/full-support/res/layout/suw_glif_recycler_template_content.xml b/library/full-support/res/layout/suw_glif_recycler_template_content.xml
index d8ae7b5..13da5db 100644
--- a/library/full-support/res/layout/suw_glif_recycler_template_content.xml
+++ b/library/full-support/res/layout/suw_glif_recycler_template_content.xml
@@ -15,10 +15,24 @@
limitations under the License.
-->
-<com.android.setupwizardlib.view.HeaderRecyclerView
+<LinearLayout
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"
- app:suwHeader="@layout/suw_glif_header" />
+ android:orientation="vertical">
+
+ <com.android.setupwizardlib.view.HeaderRecyclerView
+ android:id="@+id/suw_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:scrollbars="vertical"
+ app:suwHeader="@layout/suw_glif_header" />
+
+ <ViewStub
+ android:id="@+id/suw_layout_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java b/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java
index 8c937a1..bbd7b50 100644
--- a/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java
+++ b/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java
@@ -67,11 +67,25 @@
public static final int DIVIDER_CONDITION_BOTH = 1;
/**
- * Creates a default instance of {@link DividerItemDecoration}, using
- * {@code android:attr/listDivider} as the divider and {@code android:attr/dividerHeight} as the
- * divider height.
+ * @deprecated Use {@link #DividerItemDecoration(android.content.Context)}
*/
+ @Deprecated
public static DividerItemDecoration getDefault(Context context) {
+ return new DividerItemDecoration(context);
+ }
+
+ /* non-static section */
+
+ private Drawable mDivider;
+ private int mDividerHeight;
+ private int mDividerIntrinsicHeight;
+ @DividerCondition
+ private int mDividerCondition;
+
+ public DividerItemDecoration() {
+ }
+
+ public DividerItemDecoration(Context context) {
final TypedArray a = context.obtainStyledAttributes(R.styleable.SuwDividerItemDecoration);
final Drawable divider = a.getDrawable(
R.styleable.SuwDividerItemDecoration_android_listDivider);
@@ -82,21 +96,11 @@
DIVIDER_CONDITION_EITHER);
a.recycle();
- final DividerItemDecoration decoration = new DividerItemDecoration();
- decoration.setDivider(divider);
- decoration.setDividerHeight(dividerHeight);
- decoration.setDividerCondition(dividerCondition);
- return decoration;
+ setDivider(divider);
+ setDividerHeight(dividerHeight);
+ setDividerCondition(dividerCondition);
}
- /* non-static section */
-
- private Drawable mDivider;
- private int mDividerHeight;
- private int mDividerIntrinsicHeight;
- @DividerCondition
- private int mDividerCondition;
-
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mDivider == null) {
@@ -127,27 +131,24 @@
final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
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;
+ if (isDividerAllowedBelow(holder)) {
+ 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 RecyclerView.ViewHolder nextHolder =
parent.findViewHolderForLayoutPosition(index + 1);
- if ((nextHolder instanceof DividedViewHolder)
- && !((DividedViewHolder) nextHolder).isDividerAllowedAbove()) {
+ if (!isDividerAllowedAbove(nextHolder)) {
// Don't draw if the next view holder doesn't allow drawing above
return false;
}
@@ -156,6 +157,34 @@
}
/**
+ * Whether a divider is allowed above the view holder. The allowed values will be combined
+ * according to {@link #getDividerCondition()}. The default implementation delegates to
+ * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}, or simply allows
+ * the divider if the view holder doesn't implement {@code DividedViewHolder}. Subclasses can
+ * override this to give more information to decide whether a divider should be drawn.
+ *
+ * @return True if divider is allowed above this view holder.
+ */
+ protected boolean isDividerAllowedAbove(RecyclerView.ViewHolder viewHolder) {
+ return !(viewHolder instanceof DividedViewHolder)
+ || ((DividedViewHolder) viewHolder).isDividerAllowedAbove();
+ }
+
+ /**
+ * Whether a divider is allowed below the view holder. The allowed values will be combined
+ * according to {@link #getDividerCondition()}. The default implementation delegates to
+ * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}, or simply allows
+ * the divider if the view holder doesn't implement {@code DividedViewHolder}. Subclasses can
+ * override this to give more information to decide whether a divider should be drawn.
+ *
+ * @return True if divider is allowed below this view holder.
+ */
+ protected boolean isDividerAllowedBelow(RecyclerView.ViewHolder viewHolder) {
+ return !(viewHolder instanceof DividedViewHolder)
+ || ((DividedViewHolder) viewHolder).isDividerAllowedBelow();
+ }
+
+ /**
* Sets the drawable to be used as the divider.
*/
public void setDivider(Drawable divider) {
diff --git a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
index 509532f..476abce 100644
--- a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
+++ b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
@@ -130,7 +130,7 @@
if (mRecyclerView instanceof HeaderRecyclerView) {
mHeader = ((HeaderRecyclerView) mRecyclerView).getHeader();
}
- mDividerDecoration = DividerItemDecoration.getDefault(getContext());
+ mDividerDecoration = new DividerItemDecoration(getContext());
mRecyclerView.addItemDecoration(mDividerDecoration);
}
@@ -145,6 +145,13 @@
return super.findViewById(id);
}
+ public void setDividerItemDecoration(DividerItemDecoration decoration) {
+ mRecyclerView.removeItemDecoration(mDividerDecoration);
+ mDividerDecoration = decoration;
+ mRecyclerView.addItemDecoration(mDividerDecoration);
+ updateDivider();
+ }
+
public RecyclerView getRecyclerView() {
return mRecyclerView;
}
diff --git a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
index 5159bae..ddde2ae 100644
--- a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
+++ b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
@@ -27,7 +27,6 @@
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;
@@ -139,7 +138,7 @@
if (mRecyclerView instanceof HeaderRecyclerView) {
mHeader = ((HeaderRecyclerView) mRecyclerView).getHeader();
}
- mDividerDecoration = DividerItemDecoration.getDefault(getContext());
+ mDividerDecoration = new DividerItemDecoration(getContext());
mRecyclerView.addItemDecoration(mDividerDecoration);
}
diff --git a/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
index e0c0e46..29329b4 100644
--- a/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
+++ b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
@@ -25,6 +25,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
import com.android.setupwizardlib.DividerItemDecoration;
import com.android.setupwizardlib.R;
@@ -102,8 +103,21 @@
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ /*
+ * Returning the same view (mHeader) results in crash ".. but view is not a real child."
+ * The framework creates more than one instance of header because of "disappear"
+ * animations applied on the header and this necessitates creation of another headerview
+ * to use after the animation. We work around this restriction by returning an empty
+ * framelayout to which the header is attached using #onBindViewHolder method.
+ */
if (viewType == HEADER_VIEW_TYPE) {
- return new HeaderViewHolder(mHeader);
+ FrameLayout frameLayout = new FrameLayout(parent.getContext());
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ );
+ frameLayout.setLayoutParams(params);
+ return new HeaderViewHolder(frameLayout);
} else {
return mAdapter.onCreateViewHolder(parent, viewType);
}
@@ -115,7 +129,14 @@
if (mHeader != null) {
position--;
}
- if (position >= 0) {
+
+ if (holder instanceof HeaderViewHolder) {
+ if (mHeader.getParent() != null) {
+ ((ViewGroup) mHeader.getParent()).removeView(mHeader);
+ }
+ FrameLayout mHeaderParent = (FrameLayout) holder.itemView;
+ mHeaderParent.addView(mHeader);
+ } else {
mAdapter.onBindViewHolder(holder, position);
}
}
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 a06f6f7..da9ceb8 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
@@ -22,7 +22,6 @@
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;
diff --git a/library/main/res/layout/suw_glif_blank_template_card.xml b/library/main/res/layout/suw_glif_blank_template_card.xml
index 6f1ab9d..d120ab0 100644
--- a/library/main/res/layout/suw_glif_blank_template_card.xml
+++ b/library/main/res/layout/suw_glif_blank_template_card.xml
@@ -15,18 +15,32 @@
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suw_pattern_bg"
style="@style/SuwGlifCardBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fitsSystemWindows="true">
+ android:fitsSystemWindows="true"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
- <FrameLayout
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+ <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
android:id="@+id/suw_layout_content"
style="@style/SuwGlifCardContainer"
android:layout_width="@dimen/suw_glif_card_width"
- android:layout_height="@dimen/suw_glif_card_height"
- android:layout_centerInParent="true" />
+ android:layout_height="wrap_content"
+ android:height="@dimen/suw_glif_card_height" />
-</RelativeLayout>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_list_template_card.xml b/library/main/res/layout/suw_glif_list_template_card.xml
index 8d7fa95..5a3ba89 100644
--- a/library/main/res/layout/suw_glif_list_template_card.xml
+++ b/library/main/res/layout/suw_glif_list_template_card.xml
@@ -15,21 +15,35 @@
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suw_pattern_bg"
style="@style/SuwGlifCardBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fitsSystemWindows="true">
+ android:fitsSystemWindows="true"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
- <FrameLayout
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+ <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
style="@style/SuwGlifCardContainer"
android:layout_width="@dimen/suw_glif_card_width"
- android:layout_height="@dimen/suw_glif_card_height"
- android:layout_centerInParent="true">
+ android:layout_height="wrap_content"
+ android:height="@dimen/suw_glif_card_height">
<include layout="@layout/suw_glif_list_template_content" />
- </FrameLayout>
+ </com.android.setupwizardlib.view.IntrinsicSizeFrameLayout>
-</RelativeLayout>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_list_template_content.xml b/library/main/res/layout/suw_glif_list_template_content.xml
index 3756898..3793aeb 100644
--- a/library/main/res/layout/suw_glif_list_template_content.xml
+++ b/library/main/res/layout/suw_glif_list_template_content.xml
@@ -15,10 +15,23 @@
limitations under the License.
-->
-<com.android.setupwizardlib.view.StickyHeaderListView
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:suwHeader="@layout/suw_glif_header" />
+ android:orientation="vertical">
+
+ <com.android.setupwizardlib.view.StickyHeaderListView
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ app:suwHeader="@layout/suw_glif_header" />
+
+ <ViewStub
+ android:id="@+id/suw_layout_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_template_card.xml b/library/main/res/layout/suw_glif_template_card.xml
index d1d354f..7b190a1 100644
--- a/library/main/res/layout/suw_glif_template_card.xml
+++ b/library/main/res/layout/suw_glif_template_card.xml
@@ -15,22 +15,35 @@
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suw_pattern_bg"
style="@style/SuwGlifCardBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fitsSystemWindows="true">
+ android:fitsSystemWindows="true"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
- <FrameLayout
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+ <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
style="@style/SuwGlifCardContainer"
android:layout_width="@dimen/suw_glif_card_width"
- android:layout_height="@dimen/suw_glif_card_height"
- android:layout_centerInParent="true">
+ android:layout_height="wrap_content"
+ android:height="@dimen/suw_glif_card_height">
<include layout="@layout/suw_glif_template_content" />
- </FrameLayout>
+ </com.android.setupwizardlib.view.IntrinsicSizeFrameLayout>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
-</RelativeLayout>
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_template_content.xml b/library/main/res/layout/suw_glif_template_content.xml
index f867e50..0eda2ae 100644
--- a/library/main/res/layout/suw_glif_template_content.xml
+++ b/library/main/res/layout/suw_glif_template_content.xml
@@ -15,25 +15,39 @@
limitations under the License.
-->
-<ScrollView
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/suw_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fillViewport="true">
+ android:orientation="vertical">
- <LinearLayout
+ <ScrollView
+ android:id="@+id/suw_scroll_view"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:fillViewport="true">
- <include layout="@layout/suw_glif_header" />
-
- <FrameLayout
- android:id="@+id/suw_layout_content"
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content" />
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
- </LinearLayout>
+ <include layout="@layout/suw_glif_header" />
-</ScrollView>
+ <FrameLayout
+ android:id="@+id/suw_layout_content"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <ViewStub
+ android:id="@+id/suw_layout_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_items_default.xml b/library/main/res/layout/suw_items_default.xml
index a61b1ab..ba6b809 100644
--- a/library/main/res/layout/suw_items_default.xml
+++ b/library/main/res/layout/suw_items_default.xml
@@ -44,7 +44,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_title"
style="@style/SuwItemTitle"
android:layout_width="match_parent"
@@ -53,7 +53,7 @@
android:textAlignment="viewStart"
tools:ignore="UnusedAttribute" />
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_summary"
style="@style/SuwItemSummary"
android:layout_width="match_parent"
diff --git a/library/main/res/layout/suw_items_description.xml b/library/main/res/layout/suw_items_description.xml
index 8712be3..4e09824 100644
--- a/library/main/res/layout/suw_items_description.xml
+++ b/library/main/res/layout/suw_items_description.xml
@@ -17,12 +17,10 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- style="@style/SuwItemContainer"
+ style="?attr/suwItemDescriptionStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:paddingTop="@dimen/suw_description_margin_top"
- android:paddingBottom="@dimen/suw_description_margin_bottom_lists">
+ android:orientation="horizontal">
<FrameLayout
android:id="@+id/suw_items_icon_container"
@@ -45,7 +43,7 @@
android:layout_weight="1"
android:orientation="vertical">
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_title"
style="@style/SuwItemTitle"
android:layout_width="match_parent"
@@ -56,7 +54,7 @@
android:textAppearance="@style/TextAppearance.SuwDescription"
tools:ignore="UnusedAttribute" />
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_summary"
style="@style/SuwItemSummary"
android:layout_width="match_parent"
diff --git a/library/main/res/layout/suw_items_verbose.xml b/library/main/res/layout/suw_items_verbose.xml
index 0eaa8da..3babe40 100644
--- a/library/main/res/layout/suw_items_verbose.xml
+++ b/library/main/res/layout/suw_items_verbose.xml
@@ -26,7 +26,7 @@
android:id="@+id/suw_items_icon_container"
android:layout_width="@dimen/suw_items_icon_container_width"
android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
+ android:layout_gravity="top"
android:gravity="start">
<ImageView
@@ -43,7 +43,7 @@
android:layout_weight="1"
android:orientation="vertical">
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_title"
style="@style/SuwItemTitle.Verbose"
android:layout_width="match_parent"
@@ -52,7 +52,7 @@
android:textAlignment="viewStart"
tools:ignore="UnusedAttribute" />
- <TextView
+ <com.android.setupwizardlib.view.RichTextView
android:id="@+id/suw_items_summary"
style="@style/SuwItemSummary"
android:layout_width="match_parent"
diff --git a/library/main/res/values-sw360dp/dimens.xml b/library/main/res/values-sw360dp/dimens.xml
new file mode 100644
index 0000000..9ed9a82
--- /dev/null
+++ b/library/main/res/values-sw360dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <dimen name="suw_description_text_size">16sp</dimen>
+</resources>
diff --git a/library/main/res/values-sw600dp/dimens.xml b/library/main/res/values-sw600dp/dimens.xml
index bd05972..98c3dd7 100644
--- a/library/main/res/values-sw600dp/dimens.xml
+++ b/library/main/res/values-sw600dp/dimens.xml
@@ -22,5 +22,7 @@
<!-- Illustration -->
<item name="suw_illustration_aspect_ratio" format="float" type="dimen">0</item>
+ <dimen name="suw_header_title_size">34sp</dimen>
+ <dimen name="suw_glif_margin_sides">40dp</dimen>
</resources>
diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml
index 8501e94..73f8863 100644
--- a/library/main/res/values/attrs.xml
+++ b/library/main/res/values/attrs.xml
@@ -36,6 +36,7 @@
<attr name="suwHeader" format="reference" />
<attr name="suwHeaderText" format="string" localization="suggested" />
<attr name="suwDividerInset" format="dimension|reference" />
+ <attr name="suwItemDescriptionStyle" format="reference" />
<declare-styleable name="SuwIllustration">
<attr name="suwAspectRatio" format="float" />
@@ -65,6 +66,11 @@
<attr name="suwStatusBarBackground" format="color|reference" />
</declare-styleable>
+ <declare-styleable name="SuwMaxSizeFrameLayout">
+ <attr name="android:height" />
+ <attr name="android:width" />
+ </declare-styleable>
+
<declare-styleable name="SuwSetupWizardLayout">
<attr name="suwBackground" format="color|reference" />
<attr name="suwBackgroundTile" format="color|reference" />
diff --git a/library/main/res/values/colors.xml b/library/main/res/values/colors.xml
index a4470d1..e6ee76b 100644
--- a/library/main/res/values/colors.xml
+++ b/library/main/res/values/colors.xml
@@ -23,7 +23,7 @@
<color name="suw_color_accent_light">#ff3367d6</color>
<color name="suw_link_color_dark">#ff448aff</color>
<color name="suw_link_color_light">#ff3367d6</color>
- <color name="suw_list_item_icon_color_dark">#89ffffff</color>
+ <color name="suw_list_item_icon_color_dark">#b3ffffff</color>
<color name="suw_list_item_icon_color_light">#89000000</color>
<color name="suw_progress_bar_color_dark">#ffffcd40</color>
<color name="suw_progress_bar_color_light">#fff4b400</color>
@@ -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 55b9a45..2e89936 100644
--- a/library/main/res/values/dimens.xml
+++ b/library/main/res/values/dimens.xml
@@ -20,7 +20,7 @@
<!-- General -->
<dimen name="suw_layout_margin_sides">40dp</dimen>
<dimen name="suw_glif_margin_sides">24dp</dimen>
- <dimen name="suw_glif_margin_top">24dp</dimen>
+ <dimen name="suw_glif_margin_top">48dp</dimen>
<!-- Content styles -->
<dimen name="suw_check_box_line_spacing_extra">4sp</dimen>
@@ -39,6 +39,9 @@
<dimen name="suw_description_line_spacing_extra">4sp</dimen>
<dimen name="suw_description_text_size">16sp</dimen>
+ <dimen name="suw_description_glif_margin_top">3dp</dimen>
+ <dimen name="suw_description_glif_margin_bottom_lists">24dp</dimen>
+
<!-- Margin on the start to offset for margin in the drawable -->
<dimen name="suw_radio_button_margin_start">-6dp</dimen>
<dimen name="suw_radio_button_margin_top">0dp</dimen>
@@ -79,8 +82,8 @@
<!-- This is the extra spacing required to make the leading exactly 32sp -->
<dimen name="suw_header_title_line_spacing_extra">3.67sp</dimen>
- <dimen name="suw_glif_header_title_margin_top">24dp</dimen>
- <dimen name="suw_glif_header_title_margin_bottom">8dp</dimen>
+ <dimen name="suw_glif_header_title_margin_top">15dp</dimen>
+ <dimen name="suw_glif_header_title_margin_bottom">2dp</dimen>
<dimen name="suw_glif_icon_max_height">32dp</dimen>
diff --git a/library/main/res/values/styles.xml b/library/main/res/values/styles.xml
index 7c845a2..4f0e09a 100644
--- a/library/main/res/values/styles.xml
+++ b/library/main/res/values/styles.xml
@@ -44,6 +44,10 @@
<item name="android:textAppearance">@style/TextAppearance.SuwDescription</item>
</style>
+ <style name="SuwDescription.Glif" parent="SuwDescription">
+ <item name="android:layout_marginTop">@dimen/suw_description_glif_margin_top</item>
+ </style>
+
<style name="TextAppearance.SuwDescription.Light" parent="TextAppearance.SuwDescription">
<item name="android:fontFamily" tools:ignore="NewApi">sans-serif-light</item>
</style>
@@ -123,6 +127,17 @@
<!-- Items styles -->
+ <style name="SuwItemContainer.Description" parent="SuwItemContainer">
+ <item name="android:paddingTop">@dimen/suw_description_margin_top</item>
+ <item name="android:paddingBottom">@dimen/suw_description_margin_bottom_lists</item>
+ </style>
+
+ <style name="SuwItemContainer.Description.Glif" parent="SuwItemContainer.Description">
+ <item name="android:minHeight">0dp</item>
+ <item name="android:paddingTop">@dimen/suw_description_glif_margin_top</item>
+ <item name="android:paddingBottom">@dimen/suw_description_glif_margin_bottom_lists</item>
+ </style>
+
<style name="SuwItemContainer.Verbose" parent="SuwItemContainer">
<item name="android:paddingBottom">@dimen/suw_items_verbose_padding_vertical</item>
<item name="android:paddingTop">@dimen/suw_items_verbose_padding_vertical</item>
diff --git a/library/main/src/com/android/setupwizardlib/GlifLayout.java b/library/main/src/com/android/setupwizardlib/GlifLayout.java
index c3202ad..765e6a6 100644
--- a/library/main/src/com/android/setupwizardlib/GlifLayout.java
+++ b/library/main/src/com/android/setupwizardlib/GlifLayout.java
@@ -234,28 +234,55 @@
}
public void setProgressBarShown(boolean shown) {
- final View progressBar = findManagedViewById(R.id.suw_layout_progress);
if (shown) {
+ View progressBar = getProgressBar();
if (progressBar != null) {
progressBar.setVisibility(View.VISIBLE);
- } else {
- final ViewStub progressBarStub =
- (ViewStub) findManagedViewById(R.id.suw_layout_progress_stub);
- if (progressBarStub != null) {
- progressBarStub.inflate();
- }
}
- setProgressBarColor(mPrimaryColor);
} else {
+ View progressBar = peekProgressBar();
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
}
}
+ /**
+ * Gets the progress bar in the layout. If the progress bar has not been used before, it will be
+ * installed (i.e. inflated from its view stub).
+ *
+ * @return The progress bar of this layout. May be null only if the template used doesn't have a
+ * progress bar built-in.
+ */
+ private ProgressBar getProgressBar() {
+ final View progressBar = peekProgressBar();
+ if (progressBar == null) {
+ final ViewStub progressBarStub =
+ (ViewStub) findManagedViewById(R.id.suw_layout_progress_stub);
+ if (progressBarStub != null) {
+ progressBarStub.inflate();
+ }
+ setProgressBarColor(mPrimaryColor);
+ }
+ return peekProgressBar();
+ }
+
+ /**
+ * Gets the progress bar in the layout only if it has been installed.
+ * {@link #setProgressBarShown(boolean)} should be called before this to ensure the progress bar
+ * is set up correctly.
+ *
+ * @return The progress bar of this layout, or null if the progress bar is not installed. The
+ * null case can happen either if {@link #setProgressBarShown(boolean)} with true was
+ * not called before this, or if the template does not contain a progress bar.
+ */
+ public ProgressBar peekProgressBar() {
+ return (ProgressBar) findManagedViewById(R.id.suw_layout_progress);
+ }
+
private void setProgressBarColor(ColorStateList color) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
- final ProgressBar bar = (ProgressBar) findManagedViewById(R.id.suw_layout_progress);
+ final ProgressBar bar = peekProgressBar();
if (bar != null) {
bar.setIndeterminateTintList(color);
}
diff --git a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
index 3d0efdf..6d082a1 100644
--- a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
+++ b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
@@ -23,15 +23,28 @@
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
+import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import com.android.setupwizardlib.annotations.VisibleForTesting;
+import java.lang.ref.SoftReference;
+
+/**
+ * This class draws the GLIF pattern used as the status bar background for phones and background for
+ * tablets in GLIF layout.
+ */
public class GlifPatternDrawable extends Drawable {
+ /*
+ * This class essentially implements a simple SVG in Java code, with some special handling of
+ * scaling when given different bounds.
+ */
/* static section */
@@ -40,8 +53,28 @@
private static final float VIEWBOX_HEIGHT = 768f;
private static final float VIEWBOX_WIDTH = 1366f;
- private static final float SCALE_FOCUS_X = 200f;
- private static final float SCALE_FOCUS_Y = 175f;
+ // X coordinate of scale focus, as a fraction of of the width. (Range is 0 - 1)
+ private static final float SCALE_FOCUS_X = .146f;
+ // Y coordinate of scale focus, as a fraction of of the height. (Range is 0 - 1)
+ private static final float SCALE_FOCUS_Y = .228f;
+
+ // Alpha component of the color to be drawn, on top of the grayscale pattern. (Range is 0 - 1)
+ private static final float COLOR_ALPHA = .8f;
+ // Int version of COLOR_ALPHA. (Range is 0 - 255)
+ private static final int COLOR_ALPHA_INT = (int) (COLOR_ALPHA * 255);
+
+ // Cap the bitmap size, such that it won't hurt the performance too much
+ // and it won't crash due to a very large scale.
+ // The drawable will look blurry above this size.
+ // This is a multiplier applied on top of the viewbox size.
+ // Resulting max cache size = (1.5 x 1366, 1.5 x 768) = (2049, 1152)
+ private static final float MAX_CACHED_BITMAP_SCALE = 1.5f;
+
+ private static final int NUM_PATHS = 7;
+
+ private static SoftReference<Bitmap> sBitmapCache;
+ private static Path[] sPatternPaths;
+ private static int[] sPatternLightness;
public static GlifPatternDrawable getDefault(Context context) {
int colorPrimary = 0;
@@ -53,136 +86,194 @@
return new GlifPatternDrawable(colorPrimary);
}
+ @VisibleForTesting
+ public static void invalidatePattern() {
+ sBitmapCache = null;
+ }
+
/* non-static section */
private int mColor;
- private Paint mPaint;
- private float[] mTempHsv = new float[3];
- private Path mTempPath = new Path();
- private Bitmap mBitmap;
+ private Paint mTempPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private ColorFilter mColorFilter;
public GlifPatternDrawable(int color) {
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
setColor(color);
}
@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);
+ final Rect bounds = getBounds();
+ int drawableWidth = bounds.width();
+ int drawableHeight = bounds.height();
+ Bitmap bitmap = null;
+ if (sBitmapCache != null) {
+ bitmap = sBitmapCache.get();
}
- canvas.drawBitmap(mBitmap, 0, 0, null);
- }
+ if (bitmap != null) {
+ final int bitmapWidth = bitmap.getWidth();
+ final int bitmapHeight = bitmap.getHeight();
+ // Invalidate the cache if this drawable is bigger and we can still create a bigger
+ // cache.
+ if (drawableWidth > bitmapWidth
+ && bitmapWidth < VIEWBOX_WIDTH * MAX_CACHED_BITMAP_SCALE) {
+ bitmap = null;
+ } else if (drawableHeight > bitmapHeight
+ && bitmapHeight < VIEWBOX_HEIGHT * MAX_CACHED_BITMAP_SCALE) {
+ bitmap = null;
+ }
+ }
- private void renderOnCanvas(Canvas canvas) {
+ if (bitmap == null) {
+ // Reset the paint so it can be used to draw the paths in renderOnCanvas
+ mTempPaint.reset();
+
+ bitmap = createBitmapCache(drawableWidth, drawableHeight);
+ sBitmapCache = new SoftReference<>(bitmap);
+
+ // Reset the paint to so it can be used to draw the bitmap
+ mTempPaint.reset();
+ }
+
canvas.save();
- canvas.clipRect(getBounds());
+ canvas.clipRect(bounds);
- Color.colorToHSV(mColor, mTempHsv);
- scaleCanvasToBounds(canvas);
-
- // Draw the pattern by creating the paths, adjusting the colors and drawing them. The path
- // values are extracted from the SVG of the pattern file.
-
- mTempHsv[2] = 0.753f;
- Path p = mTempPath;
- p.reset();
- p.moveTo(1029.4f, 357.5f);
- p.lineTo(1366f, 759.1f);
- p.lineTo(1366f, 0f);
- p.lineTo(1137.7f, 0f);
- p.close();
- drawPath(canvas, p, mTempHsv);
-
- mTempHsv[2] = 0.78f;
- p.reset();
- p.moveTo(1138.1f, 0f);
- p.rLineTo(-144.8f, 768f);
- p.rLineTo(372.7f, 0f);
- p.rLineTo(0f, -524f);
- p.cubicTo(1290.7f, 121.6f, 1219.2f, 41.1f, 1178.7f, 0f);
- p.close();
- drawPath(canvas, p, mTempHsv);
-
- mTempHsv[2] = 0.804f;
- p.reset();
- p.moveTo(949.8f, 768f);
- p.rCubicTo(92.6f, -170.6f, 213f, -440.3f, 269.4f, -768f);
- p.lineTo(585f, 0f);
- p.rLineTo(2.1f, 766f);
- p.close();
- drawPath(canvas, p, mTempHsv);
-
- mTempHsv[2] = 0.827f;
- p.reset();
- p.moveTo(471.1f, 768f);
- p.rMoveTo(704.5f, 0f);
- p.cubicTo(1123.6f, 563.3f, 1027.4f, 275.2f, 856.2f, 0f);
- p.lineTo(476.4f, 0f);
- p.rLineTo(-5.3f, 768f);
- p.close();
- drawPath(canvas, p, mTempHsv);
-
- mTempHsv[2] = 0.867f;
- p.reset();
- p.moveTo(323.1f, 768f);
- p.moveTo(777.5f, 768f);
- p.cubicTo(661.9f, 348.8f, 427.2f, 21.4f, 401.2f, 25.4f);
- p.lineTo(323.1f, 768f);
- p.close();
- drawPath(canvas, p, mTempHsv);
-
- mTempHsv[2] = 0.894f;
- p.reset();
- p.moveTo(178.44286f, 766.85714f);
- p.lineTo(308.7f, 768f);
- p.cubicTo(381.7f, 604.6f, 481.6f, 344.3f, 562.2f, 0f);
- p.lineTo(0f, 0f);
- p.close();
- drawPath(canvas, p, mTempHsv);
-
- mTempHsv[2] = 0.929f;
- p.reset();
- p.moveTo(146f, 0f);
- p.lineTo(0f, 0f);
- p.lineTo(0f, 768f);
- p.lineTo(394.2f, 768f);
- p.cubicTo(327.7f, 475.3f, 228.5f, 201f, 146f, 0f);
- p.close();
- drawPath(canvas, p, mTempHsv);
+ scaleCanvasToBounds(canvas, bitmap, bounds);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
+ && canvas.isHardwareAccelerated()) {
+ mTempPaint.setColorFilter(mColorFilter);
+ canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
+ } else {
+ // Software renderer doesn't work properly with ColorMatrix filter on ALPHA_8 bitmaps.
+ canvas.drawColor(Color.BLACK);
+ mTempPaint.setColor(Color.WHITE);
+ canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
+ canvas.drawColor(mColor);
+ }
canvas.restore();
}
@VisibleForTesting
- public void scaleCanvasToBounds(Canvas canvas) {
- final Rect bounds = getBounds();
- final int height = bounds.height();
- final int width = bounds.width();
+ public Bitmap createBitmapCache(int drawableWidth, int drawableHeight) {
+ float scaleX = drawableWidth / VIEWBOX_WIDTH;
+ float scaleY = drawableHeight / VIEWBOX_HEIGHT;
+ float scale = Math.max(scaleX, scaleY);
+ scale = Math.min(MAX_CACHED_BITMAP_SCALE, scale);
- float scaleY = height / VIEWBOX_HEIGHT;
- float scaleX = width / VIEWBOX_WIDTH;
+
+ int scaledWidth = (int) (VIEWBOX_WIDTH * scale);
+ int scaledHeight = (int) (VIEWBOX_HEIGHT * scale);
+
+ // Use ALPHA_8 mask to save memory, since the pattern is grayscale only anyway.
+ Bitmap bitmap = Bitmap.createBitmap(
+ scaledWidth,
+ scaledHeight,
+ Bitmap.Config.ALPHA_8);
+ Canvas bitmapCanvas = new Canvas(bitmap);
+ renderOnCanvas(bitmapCanvas, scale);
+ return bitmap;
+ }
+
+ private void renderOnCanvas(Canvas canvas, float scale) {
+ canvas.save();
+ canvas.scale(scale, scale);
+
+ mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+
+ // Draw the pattern by creating the paths, adjusting the colors and drawing them. The path
+ // values are extracted from the SVG of the pattern file.
+
+ if (sPatternPaths == null) {
+ sPatternPaths = new Path[NUM_PATHS];
+ // Lightness values of the pattern, range 0 - 255
+ sPatternLightness = new int[] { 10, 40, 51, 66, 91, 112, 130 };
+
+ Path p = sPatternPaths[0] = new Path();
+ p.moveTo(1029.4f, 357.5f);
+ p.lineTo(1366f, 759.1f);
+ p.lineTo(1366f, 0f);
+ p.lineTo(1137.7f, 0f);
+ p.close();
+
+ p = sPatternPaths[1] = new Path();
+ p.moveTo(1138.1f, 0f);
+ p.rLineTo(-144.8f, 768f);
+ p.rLineTo(372.7f, 0f);
+ p.rLineTo(0f, -524f);
+ p.cubicTo(1290.7f, 121.6f, 1219.2f, 41.1f, 1178.7f, 0f);
+ p.close();
+
+ p = sPatternPaths[2] = new Path();
+ p.moveTo(949.8f, 768f);
+ p.rCubicTo(92.6f, -170.6f, 213f, -440.3f, 269.4f, -768f);
+ p.lineTo(585f, 0f);
+ p.rLineTo(2.1f, 766f);
+ p.close();
+
+ p = sPatternPaths[3] = new Path();
+ p.moveTo(471.1f, 768f);
+ p.rMoveTo(704.5f, 0f);
+ p.cubicTo(1123.6f, 563.3f, 1027.4f, 275.2f, 856.2f, 0f);
+ p.lineTo(476.4f, 0f);
+ p.rLineTo(-5.3f, 768f);
+ p.close();
+
+ p = sPatternPaths[4] = new Path();
+ p.moveTo(323.1f, 768f);
+ p.moveTo(777.5f, 768f);
+ p.cubicTo(661.9f, 348.8f, 427.2f, 21.4f, 401.2f, 25.4f);
+ p.lineTo(323.1f, 768f);
+ p.close();
+
+ p = sPatternPaths[5] = new Path();
+ p.moveTo(178.44286f, 766.85714f);
+ p.lineTo(308.7f, 768f);
+ p.cubicTo(381.7f, 604.6f, 481.6f, 344.3f, 562.2f, 0f);
+ p.lineTo(0f, 0f);
+ p.close();
+
+ p = sPatternPaths[6] = new Path();
+ p.moveTo(146f, 0f);
+ p.lineTo(0f, 0f);
+ p.lineTo(0f, 768f);
+ p.lineTo(394.2f, 768f);
+ p.cubicTo(327.7f, 475.3f, 228.5f, 201f, 146f, 0f);
+ p.close();
+ }
+
+ for (int i = 0; i < NUM_PATHS; i++) {
+ // Color is 0xAARRGGBB, so alpha << 24 will create a color with (alpha)% black.
+ // Although the color components don't really matter, since the backing bitmap cache is
+ // ALPHA_8.
+ mTempPaint.setColor(sPatternLightness[i] << 24);
+ canvas.drawPath(sPatternPaths[i], mTempPaint);
+ }
+
+ canvas.restore();
+ mTempPaint.reset();
+ }
+
+ @VisibleForTesting
+ public void scaleCanvasToBounds(Canvas canvas, Bitmap bitmap, Rect drawableBounds) {
+ int bitmapWidth = bitmap.getWidth();
+ int bitmapHeight = bitmap.getHeight();
+ float scaleX = drawableBounds.width() / (float) bitmapWidth;
+ float scaleY = drawableBounds.height() / (float) bitmapHeight;
+
// First scale both sides to fit independently.
canvas.scale(scaleX, scaleY);
if (scaleY > scaleX) {
- // Adjust x-scale to maintain aspect ratio, but using (200, 0) as the pivot instead,
- // so that more of the texture and less of the blank space on the left edge is seen.
- canvas.scale(scaleY / scaleX, 1f, SCALE_FOCUS_X, 0f);
- } else {
- // Adjust y-scale to maintain aspect ratio, but using (0, 175) as the pivot instead,
- // so that an intersection of two "circles" can always be seen.
- canvas.scale(1f, scaleX / scaleY, 0f, SCALE_FOCUS_Y);
+ // Adjust x-scale to maintain aspect ratio using the pivot, so that more of the texture
+ // and less of the blank space on the left edge is seen.
+ canvas.scale(scaleY / scaleX, 1f, SCALE_FOCUS_X * bitmapWidth, 0f);
+ } else if (scaleX > scaleY) {
+ // Adjust y-scale to maintain aspect ratio using the pivot, so that an intersection of
+ // two "circles" can always be seen.
+ canvas.scale(1f, scaleX / scaleY, 0f, SCALE_FOCUS_Y * bitmapHeight);
}
}
- private void drawPath(Canvas canvas, Path path, float[] hsv) {
- mPaint.setColor(Color.HSVToColor(hsv));
- canvas.drawPath(path, mPaint);
- }
-
@Override
public void setAlpha(int i) {
// Ignore
@@ -198,14 +289,29 @@
return 0;
}
+ /**
+ * Sets the color used as the base color of this pattern drawable. The alpha component of the
+ * color will be ignored.
+ */
public void setColor(int color) {
- mColor = color;
- mPaint.setColor(color);
- mBitmap = null;
+ final int r = Color.red(color);
+ final int g = Color.green(color);
+ final int b = Color.blue(color);
+ mColor = Color.argb(COLOR_ALPHA_INT, r, g, b);
+ mColorFilter = new ColorMatrixColorFilter(new float[] {
+ 0, 0, 0, 1 - COLOR_ALPHA, r * COLOR_ALPHA,
+ 0, 0, 0, 1 - COLOR_ALPHA, g * COLOR_ALPHA,
+ 0, 0, 0, 1 - COLOR_ALPHA, b * COLOR_ALPHA,
+ 0, 0, 0, 0, 255
+ });
invalidateSelf();
}
+ /**
+ * @return The color used as the base color of this pattern drawable. The alpha component of
+ * this is always 255.
+ */
public int getColor() {
- return mColor;
+ return Color.argb(255, Color.red(mColor), Color.green(mColor), Color.blue(mColor));
}
}
diff --git a/library/main/src/com/android/setupwizardlib/items/ButtonItem.java b/library/main/src/com/android/setupwizardlib/items/ButtonItem.java
index 4faeff4..2ec6489 100644
--- a/library/main/src/com/android/setupwizardlib/items/ButtonItem.java
+++ b/library/main/src/com/android/setupwizardlib/items/ButtonItem.java
@@ -114,6 +114,13 @@
throw new UnsupportedOperationException("Cannot bind to ButtonItem's view");
}
+ /**
+ * Create a button according to this button item.
+ *
+ * @param parent The parent of the button, used to retrieve the theme and context for this
+ * button.
+ * @return A button that can be added to the parent.
+ */
protected Button createButton(ViewGroup parent) {
if (mButton == null) {
Context context = parent.getContext();
@@ -122,6 +129,12 @@
}
mButton = new Button(context);
mButton.setOnClickListener(this);
+ } else {
+ if (mButton.getParent() instanceof ViewGroup) {
+ // A view cannot be added to a different parent if one already exists. Remove this
+ // button from its parent before returning.
+ ((ViewGroup) mButton.getParent()).removeView(mButton);
+ }
}
mButton.setEnabled(mEnabled);
mButton.setText(mText);
diff --git a/library/main/src/com/android/setupwizardlib/view/IntrinsicSizeFrameLayout.java b/library/main/src/com/android/setupwizardlib/view/IntrinsicSizeFrameLayout.java
new file mode 100644
index 0000000..e9ab1a7
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/view/IntrinsicSizeFrameLayout.java
@@ -0,0 +1,92 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.android.setupwizardlib.R;
+
+/**
+ * A FrameLayout subclass that has an "intrinsic size", which is the size it wants to be if that is
+ * within the constraints given by the parent. The intrinsic size can be set with the
+ * {@code android:width} and {@code android:height} attributes in XML.
+ *
+ * Note that for the intrinsic size to be meaningful, {@code android:layout_width} and/or
+ * {@code android:layout_height} will need to be {@code wrap_content}.
+ */
+public class IntrinsicSizeFrameLayout extends FrameLayout {
+
+ private int mIntrinsicHeight = 0;
+ private int mIntrinsicWidth = 0;
+
+ public IntrinsicSizeFrameLayout(Context context) {
+ super(context);
+ init(context, null, 0);
+ }
+
+ public IntrinsicSizeFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0);
+ }
+
+ @TargetApi(VERSION_CODES.HONEYCOMB)
+ public IntrinsicSizeFrameLayout(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.SuwMaxSizeFrameLayout, defStyleAttr, 0);
+ mIntrinsicHeight =
+ a.getDimensionPixelSize(R.styleable.SuwMaxSizeFrameLayout_android_height, 0);
+ mIntrinsicWidth =
+ a.getDimensionPixelSize(R.styleable.SuwMaxSizeFrameLayout_android_width, 0);
+ a.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(getIntrinsicMeasureSpec(widthMeasureSpec, mIntrinsicWidth),
+ getIntrinsicMeasureSpec(heightMeasureSpec, mIntrinsicHeight));
+ }
+
+ private int getIntrinsicMeasureSpec(int measureSpec, int intrinsicSize) {
+ if (intrinsicSize <= 0) {
+ // Intrinsic size is not set, just return the original spec
+ return measureSpec;
+ }
+ final int mode = MeasureSpec.getMode(measureSpec);
+ final int size = MeasureSpec.getSize(measureSpec);
+ if (mode == MeasureSpec.UNSPECIFIED) {
+ // Parent did not give any constraint, so we'll be the intrinsic size
+ return MeasureSpec.makeMeasureSpec(mIntrinsicHeight, MeasureSpec.EXACTLY);
+ } else if (mode == MeasureSpec.AT_MOST) {
+ // If intrinsic size is within parents constraint, take the intrinsic size.
+ // Otherwise take the parents size because that's closest to the intrinsic size.
+ return MeasureSpec.makeMeasureSpec(Math.min(size, mIntrinsicHeight),
+ MeasureSpec.EXACTLY);
+ }
+ // Parent specified EXACTLY, or in all other cases, just return the original spec
+ return measureSpec;
+ }
+}
diff --git a/library/platform/res/values-v21/styles.xml b/library/platform/res/values-v21/styles.xml
index 4a5ae8d..36b3d57 100644
--- a/library/platform/res/values-v21/styles.xml
+++ b/library/platform/res/values-v21/styles.xml
@@ -41,6 +41,7 @@
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwCardBackground">@drawable/suw_card_bg</item>
+ <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
<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>
@@ -63,6 +64,7 @@
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwCardBackground">@drawable/suw_card_bg</item>
+ <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
<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>
@@ -70,8 +72,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>
@@ -87,13 +89,14 @@
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
+ <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
<item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
</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>
@@ -109,6 +112,7 @@
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
+ <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
<item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
<item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
</style>
diff --git a/library/proguard.flags b/library/proguard.flags
new file mode 100644
index 0000000..c3a49d5
--- /dev/null
+++ b/library/proguard.flags
@@ -0,0 +1,8 @@
+#########################################
+## Keep rules
+#########################################
+
+# Allow inflating ItemHierarchies from XML
+-keep public class * extends com.android.setupwizardlib.items.ItemHierarchy {
+ public <init>(android.content.Context, android.util.AttributeSet);
+}
diff --git a/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java b/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java
index 45342d0..3490d4d 100644
--- a/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java
+++ b/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java
@@ -21,6 +21,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.android.setupwizardlib.R;
@@ -66,6 +67,18 @@
assertTrue("Default button text should be empty", TextUtils.isEmpty(button.getText()));
}
+ public void testCreateButtonTwice() {
+ TestButtonItem item = new TestButtonItem();
+ final Button button = item.createButton(mParent);
+
+ FrameLayout frameLayout = new FrameLayout(getContext());
+ frameLayout.addView(button);
+
+ final Button button2 = item.createButton(mParent);
+ assertSame("createButton should be reused", button, button2);
+ assertNull("Should be removed from parent after createButton", button2.getParent());
+ }
+
public void testSetEnabledTrue() {
TestButtonItem item = new TestButtonItem();
item.setEnabled(true);
diff --git a/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java b/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java
index 1ae620d..8100eda 100644
--- a/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java
+++ b/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java
@@ -114,6 +114,27 @@
}
}
+ @SmallTest
+ public void testPeekProgressBarNull() {
+ GlifLayout layout = new GlifLayout(mContext);
+ assertNull("PeekProgressBar should return null initially", layout.peekProgressBar());
+ }
+
+ @SmallTest
+ public void testPeekProgressBar() {
+ GlifLayout layout = new GlifLayout(mContext);
+ layout.setProgressBarShown(true);
+ assertNotNull("Peek progress bar should return the bar after setProgressBarShown(true)",
+ layout.peekProgressBar());
+ }
+
+ @SmallTest
+ public void testSetProgressBarShownInvalid() {
+ GlifLayout layout = new GlifLayout(mContext, R.layout.test_template);
+ layout.setProgressBarShown(true);
+ // This is a no-op because there is no progress bar stub
+ }
+
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 44aff73..19b4144 100644
--- a/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
+++ b/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
@@ -21,13 +21,25 @@
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.os.Debug;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
import com.android.setupwizardlib.GlifPatternDrawable;
+import junit.framework.AssertionFailedError;
+
public class GlifPatternDrawableTest extends AndroidTestCase {
+ private static final String TAG = "GlifPatternDrawableTest";
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ GlifPatternDrawable.invalidatePattern();
+ }
+
@SmallTest
public void testDraw() {
final Bitmap bitmap = Bitmap.createBitmap(1366, 768, Bitmap.Config.ARGB_8888);
@@ -37,9 +49,9 @@
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,
+ assertSameColor("Top left pixel should be #e61a1a", 0xffe61a1a, bitmap.getPixel(0, 0));
+ assertSameColor("Center pixel should be #d90d0d", 0xffd90d0d, bitmap.getPixel(683, 384));
+ assertSameColor("Bottom right pixel should be #d40808", 0xffd40808,
bitmap.getPixel(1365, 767));
}
@@ -61,9 +73,9 @@
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,
+ assertSameColor("Top left pixel should be #e61a1a", 0xffe61a1a, bitmap.getPixel(0, 0));
+ assertSameColor("Center pixel should be #d90d0d", 0xffd90d0d, bitmap.getPixel(683, 384));
+ assertSameColor("Bottom right pixel should be #d40808", 0xffd40808,
bitmap.getPixel(1365, 767));
}
@@ -72,9 +84,11 @@
final Canvas canvas = new Canvas();
Matrix expected = new Matrix(canvas.getMatrix());
+ Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
drawable.setBounds(0, 0, 683, 384); // half each side of the view box
- drawable.scaleCanvasToBounds(canvas);
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
expected.postScale(0.5f, 0.5f);
@@ -86,12 +100,14 @@
final Canvas canvas = new Canvas();
final Matrix expected = new Matrix(canvas.getMatrix());
+ Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
drawable.setBounds(0, 0, 683, 768); // half the width only
- drawable.scaleCanvasToBounds(canvas);
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
expected.postScale(1f, 1f);
- expected.postTranslate(-100f, 0f);
+ expected.postTranslate(-99.718f, 0f);
assertEquals("Matrices should match", expected, canvas.getMatrix());
}
@@ -101,13 +117,57 @@
final Canvas canvas = new Canvas();
final Matrix expected = new Matrix(canvas.getMatrix());
+ Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
drawable.setBounds(0, 0, 1366, 384); // half the height only
- drawable.scaleCanvasToBounds(canvas);
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
expected.postScale(1f, 1f);
- expected.postTranslate(0f, -87.5f);
+ expected.postTranslate(0f, -87.552f);
assertEquals("Matrices should match", expected, canvas.getMatrix());
}
+
+ @SmallTest
+ public void testScaleToCanvasMaxSize() {
+ final Canvas canvas = new Canvas();
+ final Matrix expected = new Matrix(canvas.getMatrix());
+
+ Bitmap mockBitmapCache = Bitmap.createBitmap(2049, 1152, Bitmap.Config.ALPHA_8);
+
+ final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
+ drawable.setBounds(0, 0, 1366, 768); // original viewbox size
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
+
+ expected.postScale(1 / 1.5f, 1 / 1.5f);
+ expected.postTranslate(0f, 0f);
+
+ assertEquals("Matrices should match", expected, canvas.getMatrix());
+ }
+
+ @SmallTest
+ public void testMemoryAllocation() {
+ Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo();
+ Debug.getMemoryInfo(memoryInfo);
+ final long memoryBefore = memoryInfo.getTotalPss(); // Get memory usage in KB
+
+ final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
+ drawable.setBounds(0, 0, 1366, 768);
+ drawable.createBitmapCache(2049, 1152);
+
+ Debug.getMemoryInfo(memoryInfo);
+ final long memoryAfter = memoryInfo.getTotalPss();
+ Log.i(TAG, "Memory allocated for bitmap cache: " + (memoryAfter - memoryBefore));
+ assertTrue("Memory allocation should not exceed 5MB", memoryAfter < memoryBefore + 5000);
+ }
+
+ private void assertSameColor(String message, int expected, int actual) {
+ try {
+ assertEquals(expected, actual);
+ } catch (AssertionFailedError e) {
+ throw new AssertionFailedError(message + " expected <#" + Integer.toHexString(expected)
+ + "> but found <#" + Integer.toHexString(actual) + "> instead");
+ }
+ }
}