Add ListMixin

Add ListMixin to allow common interface to a TemplateLayout containing
lists.

Test: ./gradlew connectedAndroidTest
Change-Id: I6ebd2c5a83b14db534cf66e5de7fe12584441537
This commit is contained in:
Maurice Lam 2017-01-26 14:33:16 -08:00
parent c274e39f36
commit d349adb394
10 changed files with 406 additions and 141 deletions

View file

@ -86,6 +86,7 @@
<item name="colorPrimary">@color/suw_color_accent_glif_dark</item>
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwDividerInset">@dimen/suw_items_glif_icon_divider_inset</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>
@ -112,6 +113,7 @@
<item name="colorPrimary">@color/suw_color_accent_glif_light</item>
<item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
<item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
<item name="suwDividerInset">@dimen/suw_items_glif_icon_divider_inset</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>

View file

@ -41,7 +41,7 @@ import com.android.setupwizardlib.view.NavigationBar;
* {@code android:entries} can also be used to specify an
* {@link com.android.setupwizardlib.items.ItemHierarchy} to be used with this layout in XML.
*
* @see SetupWizardItemsLayout
* @see SetupWizardListLayout
*/
public class SetupWizardRecyclerLayout extends SetupWizardLayout {

View file

@ -54,11 +54,6 @@
<attr name="suwColorPrimary" />
</declare-styleable>
<declare-styleable name="SuwGlifListLayout">
<attr name="android:entries" />
<attr name="suwDividerInset" />
</declare-styleable>
<declare-styleable name="SuwStatusBarBackgroundLayout">
<attr name="suwStatusBarBackground" format="color|reference" />
</declare-styleable>
@ -83,14 +78,6 @@
<attr name="suwContainer" format="reference" />
</declare-styleable>
<declare-styleable name="SuwSetupWizardListLayout">
<attr name="suwDividerInset" />
</declare-styleable>
<declare-styleable name="SuwSetupWizardItemsLayout">
<attr name="android:entries" />
</declare-styleable>
<declare-styleable name="SuwAbstractItem">
<attr name="android:id" />
</declare-styleable>
@ -129,4 +116,9 @@
<attr name="android:icon" />
</declare-styleable>
<declare-styleable name="SuwListMixin">
<attr name="android:entries" />
<attr name="suwDividerInset" />
</declare-styleable>
</resources>

View file

@ -18,22 +18,16 @@ package com.android.setupwizardlib;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HeaderViewListAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.android.setupwizardlib.items.ItemAdapter;
import com.android.setupwizardlib.items.ItemGroup;
import com.android.setupwizardlib.items.ItemInflater;
import com.android.setupwizardlib.util.DrawableLayoutDirectionHelper;
import com.android.setupwizardlib.template.ListMixin;
/**
* A GLIF themed layout with a ListView. {@code android:entries} can also be used to specify an
@ -47,10 +41,7 @@ public class GlifListLayout extends GlifLayout {
/* non-static section */
private ListView mListView;
private Drawable mDivider;
private Drawable mDefaultDivider;
private int mDividerInset;
private ListMixin mListMixin;
public GlifListLayout(Context context) {
this(context, 0, 0);
@ -77,30 +68,14 @@ public class GlifListLayout extends GlifLayout {
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwGlifListLayout,
defStyleAttr, 0);
final int xml = a.getResourceId(R.styleable.SuwGlifListLayout_android_entries, 0);
if (xml != 0) {
final ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml);
setAdapter(new ItemAdapter(inflated));
}
int dividerInset =
a.getDimensionPixelSize(R.styleable.SuwGlifListLayout_suwDividerInset, 0);
if (dividerInset == 0) {
dividerInset = getResources()
.getDimensionPixelSize(R.dimen.suw_items_glif_icon_divider_inset);
}
setDividerInset(dividerInset);
a.recycle();
mListMixin = new ListMixin(this, attrs, defStyleAttr);
registerMixin(ListMixin.class, mListMixin);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mDivider == null) {
// Update divider in case layout direction has just been resolved
updateDivider();
}
mListMixin.onLayout();
}
@Override
@ -119,25 +94,16 @@ public class GlifListLayout extends GlifLayout {
return super.findContainer(containerId);
}
@Override
protected void onTemplateInflated() {
mListView = (ListView) findViewById(android.R.id.list);
}
public ListView getListView() {
return mListView;
return mListMixin.getListView();
}
public void setAdapter(ListAdapter adapter) {
getListView().setAdapter(adapter);
mListMixin.setAdapter(adapter);
}
public ListAdapter getAdapter() {
final ListAdapter adapter = getListView().getAdapter();
if (adapter instanceof HeaderViewListAdapter) {
return ((HeaderViewListAdapter) adapter).getWrappedAdapter();
}
return adapter;
return mListMixin.getAdapter();
}
/**
@ -147,33 +113,24 @@ public class GlifListLayout extends GlifLayout {
* @param inset The number of pixels to inset on the "start" side of the list divider. Typically
* this will be either {@code @dimen/suw_items_glif_icon_divider_inset} or
* {@code @dimen/suw_items_glif_text_divider_inset}.
*
* @see ListMixin#setDividerInset(int)
*/
public void setDividerInset(int inset) {
mDividerInset = inset;
updateDivider();
mListMixin.setDividerInset(inset);
}
/**
* @see ListMixin#getDividerInset()
*/
public int getDividerInset() {
return mDividerInset;
}
private void updateDivider() {
boolean shouldUpdate = true;
if (Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
shouldUpdate = isLayoutDirectionResolved();
}
if (shouldUpdate) {
final ListView listView = getListView();
if (mDefaultDivider == null) {
mDefaultDivider = listView.getDivider();
}
mDivider = DrawableLayoutDirectionHelper.createRelativeInsetDrawable(mDefaultDivider,
mDividerInset /* start */, 0 /* top */, 0 /* end */, 0 /* bottom */, this);
listView.setDivider(mDivider);
}
return mListMixin.getDividerInset();
}
/**
* @see ListMixin#getDivider()
*/
public Drawable getDivider() {
return mDivider;
return mListMixin.getDivider();
}
}

View file

@ -17,40 +17,33 @@
package com.android.setupwizardlib;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.ListAdapter;
import com.android.setupwizardlib.items.ItemAdapter;
import com.android.setupwizardlib.items.ItemGroup;
import com.android.setupwizardlib.items.ItemInflater;
/**
* @deprecated Use {@link SetupWizardListLayout} instead.
*/
@Deprecated
public class SetupWizardItemsLayout extends SetupWizardListLayout {
private ItemAdapter mAdapter;
public SetupWizardItemsLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public SetupWizardItemsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwSetupWizardItemsLayout,
defStyleAttr, 0);
int xml = a.getResourceId(R.styleable.SuwSetupWizardItemsLayout_android_entries, 0);
if (xml != 0) {
ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml);
mAdapter = new ItemAdapter(inflated);
setAdapter(mAdapter);
}
a.recycle();
}
@Override
@Nullable
public ItemAdapter getAdapter() {
return mAdapter;
final ListAdapter adapter = super.getAdapter();
if (adapter instanceof ItemAdapter) {
return (ItemAdapter) adapter;
}
return null;
}
}

View file

@ -18,9 +18,7 @@ package com.android.setupwizardlib;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.util.Log;
@ -30,17 +28,15 @@ import android.view.ViewGroup;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.android.setupwizardlib.util.DrawableLayoutDirectionHelper;
import com.android.setupwizardlib.template.ListMixin;
import com.android.setupwizardlib.util.ListViewRequireScrollHelper;
import com.android.setupwizardlib.view.NavigationBar;
public class SetupWizardListLayout extends SetupWizardLayout {
private static final String TAG = "SetupWizardListLayout";
private ListView mListView;
private Drawable mDivider;
private Drawable mDefaultDivider;
private int mDividerInset;
private ListMixin mListMixin;
public SetupWizardListLayout(Context context) {
this(context, 0, 0);
@ -67,12 +63,8 @@ public class SetupWizardListLayout extends SetupWizardLayout {
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.SuwSetupWizardListLayout, defStyleAttr, 0);
int dividerInset =
a.getDimensionPixelSize(R.styleable.SuwSetupWizardListLayout_suwDividerInset, 0);
setDividerInset(dividerInset);
a.recycle();
mListMixin = new ListMixin(this, attrs, defStyleAttr);
registerMixin(ListMixin.class, mListMixin);
}
@Override
@ -94,23 +86,19 @@ public class SetupWizardListLayout extends SetupWizardLayout {
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mDivider == null) {
// Update divider in case layout direction has just been resolved
updateDivider();
}
}
@Override
protected void onTemplateInflated() {
mListView = (ListView) findViewById(android.R.id.list);
mListMixin.onLayout();
}
public ListView getListView() {
return mListView;
return mListMixin.getListView();
}
public void setAdapter(ListAdapter adapter) {
getListView().setAdapter(adapter);
mListMixin.setAdapter(adapter);
}
public ListAdapter getAdapter() {
return mListMixin.getAdapter();
}
@Override
@ -132,33 +120,24 @@ public class SetupWizardListLayout extends SetupWizardLayout {
* @param inset The number of pixels to inset on the "start" side of the list divider. Typically
* this will be either {@code @dimen/suw_items_icon_divider_inset} or
* {@code @dimen/suw_items_text_divider_inset}.
*
* @see ListMixin#setDividerInset(int)
*/
public void setDividerInset(int inset) {
mDividerInset = inset;
updateDivider();
mListMixin.setDividerInset(inset);
}
/**
* @see ListMixin#getDividerInset()
*/
public int getDividerInset() {
return mDividerInset;
}
private void updateDivider() {
boolean shouldUpdate = true;
if (Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
shouldUpdate = isLayoutDirectionResolved();
}
if (shouldUpdate) {
final ListView listView = getListView();
if (mDefaultDivider == null) {
mDefaultDivider = listView.getDivider();
}
mDivider = DrawableLayoutDirectionHelper.createRelativeInsetDrawable(mDefaultDivider,
mDividerInset /* start */, 0 /* top */, 0 /* end */, 0 /* bottom */, this);
listView.setDivider(mDivider);
}
return mListMixin.getDividerInset();
}
/**
* @see ListMixin#getDivider()
*/
public Drawable getDivider() {
return mDivider;
return mListMixin.getDivider();
}
}

View file

@ -27,7 +27,7 @@ import android.widget.TextView;
import com.android.setupwizardlib.R;
/**
* Definition of an item in SetupWizardItemsLayout. An item is usually defined in XML and inflated
* Definition of an item in an {@link ItemHierarchy}. An item is usually defined in XML and inflated
* using {@link ItemInflater}.
*/
public class Item extends AbstractItem {

View file

@ -0,0 +1,188 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.setupwizardlib.template;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.HeaderViewListAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.TemplateLayout;
import com.android.setupwizardlib.items.ItemAdapter;
import com.android.setupwizardlib.items.ItemGroup;
import com.android.setupwizardlib.items.ItemInflater;
import com.android.setupwizardlib.util.DrawableLayoutDirectionHelper;
/**
* A {@link Mixin} for interacting with ListViews.
*/
public class ListMixin implements Mixin {
private TemplateLayout mTemplateLayout;
@Nullable
private ListView mListView;
private Drawable mDivider;
private Drawable mDefaultDivider;
private int mDividerInset;
/**
* @param layout The layout this mixin belongs to.
*/
public ListMixin(@NonNull TemplateLayout layout, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
mTemplateLayout = layout;
final Context context = layout.getContext();
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.SuwListMixin, defStyleAttr, 0);
final int entries = a.getResourceId(R.styleable.SuwListMixin_android_entries, 0);
if (entries != 0) {
final ItemGroup inflated =
(ItemGroup) new ItemInflater(context).inflate(entries);
setAdapter(new ItemAdapter(inflated));
}
int dividerInset =
a.getDimensionPixelSize(R.styleable.SuwListMixin_suwDividerInset, 0);
setDividerInset(dividerInset);
a.recycle();
}
/**
* @return The list view contained in the layout, as marked by {@code @android:id/list}. This
* will return {@code null} if the list doesn't exist in the layout.
*/
public ListView getListView() {
return getListViewInternal();
}
// Client code can assume getListView() will not be null if they know their template contains
// the list, but this mixin cannot. Any usages of getListView in this mixin needs null checks.
@Nullable
private ListView getListViewInternal() {
if (mListView == null) {
final View list = mTemplateLayout.findManagedViewById(android.R.id.list);
if (list instanceof ListView) {
mListView = (ListView) list;
}
}
return mListView;
}
/**
* List mixin needs to update the dividers if the layout direction has changed. This method
* should be called when {@link View#onLayout(boolean, int, int, int, int)} of the template
* is called.
*/
public void onLayout() {
if (mDivider == null) {
// Update divider in case layout direction has just been resolved
updateDivider();
}
}
/**
* Gets the adapter of the list view in this layout. If the adapter is a HeaderViewListAdapter,
* this method will unwrap it and return the underlying adapter.
*
* @return The adapter, or {@code null} if there is no list, or if the list has no adapter.
*/
public ListAdapter getAdapter() {
final ListView listView = getListViewInternal();
if (listView != null) {
final ListAdapter adapter = listView.getAdapter();
if (adapter instanceof HeaderViewListAdapter) {
return ((HeaderViewListAdapter) adapter).getWrappedAdapter();
}
return adapter;
}
return null;
}
/**
* Sets the adapter on the list view in this layout.
*/
public void setAdapter(ListAdapter adapter) {
final ListView listView = getListViewInternal();
if (listView != null) {
listView.setAdapter(adapter);
}
}
/**
* Sets the start inset of the divider. This will use the default divider drawable set in the
* theme and inset it {@code inset} pixels to the right (or left in RTL layouts).
*
* @param inset The number of pixels to inset on the "start" side of the list divider. Typically
* this will be either {@code @dimen/suw_items_glif_icon_divider_inset} or
* {@code @dimen/suw_items_glif_text_divider_inset}.
*/
public void setDividerInset(int inset) {
mDividerInset = inset;
updateDivider();
}
/**
* @return The number of pixels inset on the start side of the divider.
*/
public int getDividerInset() {
return mDividerInset;
}
private void updateDivider() {
final ListView listView = getListViewInternal();
if (listView == null) {
return;
}
boolean shouldUpdate = true;
if (Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
shouldUpdate = mTemplateLayout.isLayoutDirectionResolved();
}
if (shouldUpdate) {
if (mDefaultDivider == null) {
mDefaultDivider = listView.getDivider();
}
mDivider = DrawableLayoutDirectionHelper.createRelativeInsetDrawable(
mDefaultDivider,
mDividerInset /* start */,
0 /* top */,
0 /* end */,
0 /* bottom */,
mTemplateLayout);
listView.setDivider(mDivider);
}
}
/**
* @return The drawable used as the divider.
*/
public Drawable getDivider() {
return mDivider;
}
}

View file

@ -89,6 +89,7 @@
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwDividerInset">@dimen/suw_items_glif_icon_divider_inset</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>
@ -112,6 +113,7 @@
<item name="android:windowDisablePreview">true</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="suwDividerInset">@dimen/suw_items_glif_icon_divider_inset</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>

View file

@ -0,0 +1,152 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.setupwizardlib.template;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.android.setupwizardlib.TemplateLayout;
import com.android.setupwizardlib.test.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ListMixinTest {
private Context mContext;
private TemplateLayout mTemplateLayout;
private ListView mListView;
@Mock
private ListAdapter mAdapter;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = InstrumentationRegistry.getTargetContext();
mTemplateLayout = spy(new TemplateLayout(mContext, R.layout.test_template,
R.id.suw_layout_content));
mListView = spy(new ListView(mContext));
doReturn(1).when(mAdapter).getViewTypeCount();
doReturn(mListView).when(mTemplateLayout)
.findManagedViewById(eq(android.R.id.list));
doReturn(true).when(mTemplateLayout).isLayoutDirectionResolved();
}
@Test
public void testGetListView() {
ListMixin mixin = new ListMixin(mTemplateLayout, null, 0);
assertSame(mListView, mixin.getListView());
}
@Test
public void testGetAdapter() {
mListView.setAdapter(mAdapter);
ListMixin mixin = new ListMixin(mTemplateLayout, null, 0);
assertSame(mAdapter, mixin.getAdapter());
}
@Test
public void testSetAdapter() {
assertNull(mListView.getAdapter());
ListMixin mixin = new ListMixin(mTemplateLayout, null, 0);
mixin.setAdapter(mAdapter);
assertSame(mAdapter, mListView.getAdapter());
}
@Test
public void testDividerInset() {
ListMixin mixin = new ListMixin(mTemplateLayout, null, 0);
mixin.setDividerInset(123);
assertEquals(123, mixin.getDividerInset());
final Drawable divider = mListView.getDivider();
InsetDrawable insetDrawable = (InsetDrawable) divider;
Rect rect = new Rect();
insetDrawable.getPadding(rect);
assertEquals(new Rect(123, 0, 0, 0), rect);
}
@Test
public void testDividerInsetRtl() {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
doReturn(View.LAYOUT_DIRECTION_RTL).when(mTemplateLayout).getLayoutDirection();
ListMixin mixin = new ListMixin(mTemplateLayout, null, 0);
mixin.setDividerInset(123);
assertEquals(123, mixin.getDividerInset());
final Drawable divider = mListView.getDivider();
InsetDrawable insetDrawable = (InsetDrawable) divider;
Rect rect = new Rect();
insetDrawable.getPadding(rect);
assertEquals(new Rect(0, 0, 123, 0), rect);
}
// else the test passes
}
@Test
public void testNoList() {
doReturn(null).when(mTemplateLayout).findManagedViewById(eq(android.R.id.list));
ListMixin mixin = new ListMixin(mTemplateLayout, null, 0);
mixin.setAdapter(mAdapter);
mixin.setDividerInset(123);
assertNull(mixin.getListView());
assertNull(mixin.getAdapter());
mixin.getDividerInset(); // Test that it doesn't crash. The return value is not significant.
assertNull(mixin.getDivider());
verifyNoMoreInteractions(mListView);
}
}