diff options
9 files changed, 510 insertions, 12 deletions
diff --git a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java new file mode 100644 index 0000000..b7e2171 --- /dev/null +++ b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java @@ -0,0 +1,130 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build.VERSION_CODES; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +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; +import com.android.setupwizardlib.items.RecyclerItemAdapter; +import com.android.setupwizardlib.view.HeaderRecyclerView; + +/** + * A GLIF themed layout with a 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. + */ +public class GlifRecyclerLayout extends GlifLayout { + + private RecyclerView mRecyclerView; + private TextView mHeaderTextView; + private ImageView mIconView; + + public GlifRecyclerLayout(Context context) { + this(context, 0, 0); + } + + public GlifRecyclerLayout(Context context, int template) { + this(context, template, 0); + } + + public GlifRecyclerLayout(Context context, int template, int containerId) { + super(context, template, containerId); + init(context, null, 0); + } + + public GlifRecyclerLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + @TargetApi(VERSION_CODES.HONEYCOMB) + public GlifRecyclerLayout(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.SuwGlifRecyclerLayout, defStyleAttr, 0); + final int xml = a.getResourceId(R.styleable.SuwGlifRecyclerLayout_android_entries, 0); + if (xml != 0) { + final ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml); + setAdapter(new RecyclerItemAdapter(inflated)); + } + a.recycle(); + } + + @Override + protected View onInflateTemplate(LayoutInflater inflater, int template) { + if (template == 0) { + template = R.layout.suw_glif_recycler_template; + } + return super.onInflateTemplate(inflater, template); + } + + @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())); + 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); + } + } + + @Override + protected TextView getHeaderTextView() { + return mHeaderTextView; + } + + @Override + protected ImageView getIconView() { + return mIconView; + } + + public RecyclerView getRecyclerView() { + return mRecyclerView; + } + + public void setAdapter(RecyclerView.Adapter adapter) { + getRecyclerView().setAdapter(adapter); + } + + public RecyclerView.Adapter getAdapter() { + return getRecyclerView().getAdapter(); + } +} 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 d3da5a1..2624d98 100644 --- a/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java +++ b/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java @@ -53,7 +53,6 @@ public class RecyclerItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewH public RecyclerItemAdapter(ItemHierarchy hierarchy) { mItemHierarchy = hierarchy; mItemHierarchy.registerObserver(this); - setHasStableIds(true); } public IItem getItem(int position) { @@ -64,7 +63,8 @@ public class RecyclerItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewH public long getItemId(int position) { IItem mItem = getItem(position); if (mItem instanceof AbstractItem) { - return ((AbstractItem) mItem).getId(); + final int id = ((AbstractItem) mItem).getId(); + return id > 0 ? id : RecyclerView.NO_ID; } else { return RecyclerView.NO_ID; } diff --git a/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java new file mode 100644 index 0000000..d96b05f --- /dev/null +++ b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java @@ -0,0 +1,187 @@ +/* + * 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.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import com.android.setupwizardlib.R; +import com.android.setupwizardlib.annotations.VisibleForTesting; + +/** + * A RecyclerView that can display a header item at the start of the list. The header can be set by + * {@code app:suwHeader} in XML. Note that the header will not be inflated until a layout manager + * is set. + */ +public class HeaderRecyclerView extends RecyclerView { + + private static class HeaderViewHolder extends ViewHolder { + + public HeaderViewHolder(View itemView) { + super(itemView); + } + } + + /** + * An adapter that can optionally add one header item to the RecyclerView. + */ + public static class HeaderAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + + private RecyclerView.Adapter mAdapter; + private View mHeader; + private static final int HEADER_VIEW_TYPE = Integer.MAX_VALUE; + + public HeaderAdapter(RecyclerView.Adapter adapter) { + mAdapter = adapter; + setHasStableIds(mAdapter.hasStableIds()); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == HEADER_VIEW_TYPE) { + return new HeaderViewHolder(mHeader); + } else { + return mAdapter.onCreateViewHolder(parent, viewType); + } + } + + @Override + @SuppressWarnings("unchecked") + public void onBindViewHolder(ViewHolder holder, int position) { + if (mHeader != null) { + position--; + } + if (position >= 0) { + mAdapter.onBindViewHolder(holder, position); + } + } + + @Override + public int getItemViewType(int position) { + if (mHeader != null) { + position--; + } + if (position < 0) { + return HEADER_VIEW_TYPE; + } + return mAdapter.getItemViewType(position); + } + + @Override + public int getItemCount() { + int count = mAdapter.getItemCount(); + if (mHeader != null) { + count++; + } + return count; + } + + @Override + public long getItemId(int position) { + if (mHeader != null) { + position--; + } + if (position < 0) { + return Long.MAX_VALUE; + } + return mAdapter.getItemId(position); + } + + public void setHeader(View header) { + mHeader = header; + } + + @VisibleForTesting + public RecyclerView.Adapter getWrappedAdapter() { + return mAdapter; + } + } + + private View mHeader; + private int mHeaderRes; + + public HeaderRecyclerView(Context context) { + super(context); + init(null, 0); + } + + public HeaderRecyclerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs, defStyleAttr); + } + + private void init(AttributeSet attrs, int defStyleAttr) { + final TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.SuwHeaderRecyclerView, defStyleAttr, 0); + mHeaderRes = a.getResourceId(R.styleable.SuwHeaderRecyclerView_suwHeader, 0); + a.recycle(); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + // Decoration-only headers should not count as an item for accessibility, adjust the + // accessibility event to account for that. + final int numberOfHeaders = mHeader != null ? 1 : 0; + event.setItemCount(event.getItemCount() - numberOfHeaders); + event.setFromIndex(Math.max(event.getFromIndex() - numberOfHeaders, 0)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + event.setToIndex(Math.max(event.getToIndex() - numberOfHeaders, 0)); + } + } + + /** + * Gets the header view of this RecyclerView, or {@code null} if there are no headers. + */ + public View getHeader() { + return mHeader; + } + + @Override + public void setLayoutManager(LayoutManager layout) { + super.setLayoutManager(layout); + if (layout != null && mHeader == null && mHeaderRes != 0) { + // Inflating a child view requires the layout manager to be set. Check here to see if + // any header item is specified in XML and inflate them. + final LayoutInflater inflater = LayoutInflater.from(getContext()); + mHeader = inflater.inflate(mHeaderRes, this, false); + } + } + + @Override + public void setAdapter(Adapter adapter) { + if (mHeader != null && adapter != null) { + final HeaderAdapter headerAdapter = new HeaderAdapter(adapter); + headerAdapter.setHeader(mHeader); + adapter = headerAdapter; + } + super.setAdapter(adapter); + } +} diff --git a/library/full-support/test/res/layout/test_glif_recycler_layout.xml b/library/full-support/test/res/layout/test_glif_recycler_layout.xml new file mode 100644 index 0000000..45a3928 --- /dev/null +++ b/library/full-support/test/res/layout/test_glif_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.GlifRecyclerLayout + 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/GlifRecyclerLayoutTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java new file mode 100644 index 0000000..74f5e73 --- /dev/null +++ b/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java @@ -0,0 +1,126 @@ +/* + * 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.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 android.widget.ImageView; +import android.widget.TextView; + +import com.android.setupwizardlib.GlifRecyclerLayout; +import com.android.setupwizardlib.view.HeaderRecyclerView; + +public class GlifRecyclerLayoutTest 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() { + GlifRecyclerLayout layout = new TestLayout(mContext); + assertRecyclerTemplateInflated(layout); + } + + @SmallTest + public void testInflateFromXml() { + LayoutInflater inflater = LayoutInflater.from(mContext); + GlifRecyclerLayout layout = (GlifRecyclerLayout) + inflater.inflate(R.layout.test_glif_recycler_layout, null); + assertRecyclerTemplateInflated(layout); + } + + @SmallTest + public void testGetRecyclerView() { + GlifRecyclerLayout layout = new TestLayout(mContext); + assertRecyclerTemplateInflated(layout); + assertNotNull("getRecyclerView should not be null", layout.getRecyclerView()); + } + + @SmallTest + public void testAdapter() { + GlifRecyclerLayout 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(); + if (gotAdapter instanceof HeaderRecyclerView.HeaderAdapter) { + assertSame("Adapter got from GlifRecyclerLayout should be same as set", + adapter, ((HeaderRecyclerView.HeaderAdapter) gotAdapter).getWrappedAdapter()); + } else { + assertSame("Adapter got from GlifRecyclerLayout should be same as set", + adapter, gotAdapter); + } + } + + private void assertRecyclerTemplateInflated(GlifRecyclerLayout 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).getHeaderTextView()); + assertNotNull("Icon view should not be null", ((TestLayout) layout).getIconView()); + } + } + + // Make some methods public for testing + public static class TestLayout extends GlifRecyclerLayout { + + public TestLayout(Context context) { + super(context); + } + + @Override + public TextView getHeaderTextView() { + return super.getHeaderTextView(); + } + + @Override + public ImageView getIconView() { + return super.getIconView(); + } + } +} diff --git a/library/main/res/layout/suw_glif_recycler_template.xml b/library/main/res/layout/suw_glif_recycler_template.xml new file mode 100644 index 0000000..d8ae7b5 --- /dev/null +++ b/library/main/res/layout/suw_glif_recycler_template.xml @@ -0,0 +1,24 @@ +<?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. +--> + +<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" + app:suwHeader="@layout/suw_glif_header" /> diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml index 3ce3218..1ca1a17 100644 --- a/library/main/res/values/attrs.xml +++ b/library/main/res/values/attrs.xml @@ -28,6 +28,7 @@ <attr name="suwNavBarTheme" format="reference" /> <!-- Custom view attributes --> + <attr name="suwHeader" format="reference" /> <attr name="suwHeaderText" format="string" localization="suggested" /> <declare-styleable name="SuwIllustration"> @@ -35,7 +36,11 @@ </declare-styleable> <declare-styleable name="SuwStickyHeaderListView"> - <attr name="suwHeader" format="reference" /> + <attr name="suwHeader" /> + </declare-styleable> + + <declare-styleable name="SuwHeaderRecyclerView"> + <attr name="suwHeader" /> </declare-styleable> <declare-styleable name="SuwGlifLayout"> @@ -48,6 +53,10 @@ <attr name="android:entries" /> </declare-styleable> + <declare-styleable name="SuwGlifRecyclerLayout"> + <attr name="android:entries" /> + </declare-styleable> + <declare-styleable name="SuwSetupWizardLayout"> <attr name="suwBackground" format="color|reference" /> <attr name="suwBackgroundTile" format="color|reference" /> diff --git a/library/main/src/com/android/setupwizardlib/GlifLayout.java b/library/main/src/com/android/setupwizardlib/GlifLayout.java index 09fafec..294cc43 100644 --- a/library/main/src/com/android/setupwizardlib/GlifLayout.java +++ b/library/main/src/com/android/setupwizardlib/GlifLayout.java @@ -126,34 +126,35 @@ public class GlifLayout extends TemplateLayout { return view instanceof ScrollView ? (ScrollView) view : null; } + protected TextView getHeaderTextView() { + return (TextView) findViewById(R.id.suw_layout_title); + } + public void setHeaderText(int title) { - final TextView titleView = (TextView) findViewById(R.id.suw_layout_title); - if (titleView != null) { - titleView.setText(title); - } + setHeaderText(getContext().getResources().getText(title)); } public void setHeaderText(CharSequence title) { - final TextView titleView = (TextView) findViewById(R.id.suw_layout_title); + final TextView titleView = getHeaderTextView(); if (titleView != null) { titleView.setText(title); } } public CharSequence getHeaderText() { - final TextView titleView = (TextView) findViewById(R.id.suw_layout_title); + final TextView titleView = getHeaderTextView(); return titleView != null ? titleView.getText() : null; } public void setHeaderColor(ColorStateList color) { - final TextView titleView = (TextView) findViewById(R.id.suw_layout_title); + final TextView titleView = getHeaderTextView(); if (titleView != null) { titleView.setTextColor(color); } } public ColorStateList getHeaderColor() { - final TextView titleView = (TextView) findViewById(R.id.suw_layout_title); + final TextView titleView = getHeaderTextView(); return titleView != null ? titleView.getTextColors() : null; } @@ -169,7 +170,7 @@ public class GlifLayout extends TemplateLayout { return iconView != null ? iconView.getDrawable() : null; } - private ImageView getIconView() { + protected ImageView getIconView() { return (ImageView) findViewById(R.id.suw_layout_icon); } } diff --git a/library/rules.gradle b/library/rules.gradle index 7660853..e99c994 100644 --- a/library/rules.gradle +++ b/library/rules.gradle @@ -98,6 +98,7 @@ android { androidTestFullSupport { java.srcDirs = ['full-support/test/src'] + res.srcDirs = ['full-support/test/res'] } } } |