diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml index 7e9753d..77ea36d 100644 --- a/library/main/res/values/attrs.xml +++ b/library/main/res/values/attrs.xml @@ -37,7 +37,6 @@ - @@ -48,4 +47,9 @@ + + + + + diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java index 9caff6c..73cc1bd 100644 --- a/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java +++ b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java @@ -36,64 +36,47 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; import android.widget.TextView; -import com.android.setupwizardlib.annotations.Keep; import com.android.setupwizardlib.util.RequireScrollHelper; import com.android.setupwizardlib.view.BottomScrollView; import com.android.setupwizardlib.view.Illustration; import com.android.setupwizardlib.view.NavigationBar; -public class SetupWizardLayout extends FrameLayout { +public class SetupWizardLayout extends TemplateLayout { private static final String TAG = "SetupWizardLayout"; - /** - * The container of the actual content. This will be a view in the template, which child views - * will be added to when {@link #addView(android.view.View)} is called. This will be the layout - * in the template that has the ID of {@link #getContainerId()}. For the default implementation - * of SetupWizardLayout, that would be @id/suw_layout_content. - */ - private ViewGroup mContainer; - public SetupWizardLayout(Context context) { - super(context); - init(0, null, R.attr.suwLayoutTheme); + super(context, 0, 0); + init(null, R.attr.suwLayoutTheme); } public SetupWizardLayout(Context context, int template) { - super(context); - init(template, null, R.attr.suwLayoutTheme); + this(context, template, 0); + } + + public SetupWizardLayout(Context context, int template, int containerId) { + super(context, template, containerId); + init(null, R.attr.suwLayoutTheme); } public SetupWizardLayout(Context context, AttributeSet attrs) { super(context, attrs); - init(0, attrs, R.attr.suwLayoutTheme); + init(attrs, R.attr.suwLayoutTheme); } @TargetApi(VERSION_CODES.HONEYCOMB) public SetupWizardLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(0, attrs, defStyleAttr); - } - - @TargetApi(VERSION_CODES.HONEYCOMB) - public SetupWizardLayout(Context context, int template, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(template, attrs, defStyleAttr); + init(attrs, defStyleAttr); } // All the constructors delegate to this init method. The 3-argument constructor is not // available in LinearLayout before v11, so call super with the exact same arguments. - private void init(int template, AttributeSet attrs, int defStyleAttr) { + private void init(AttributeSet attrs, int defStyleAttr) { final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SuwSetupWizardLayout, defStyleAttr, 0); - if (template == 0) { - template = a.getResourceId(R.styleable.SuwSetupWizardLayout_android_layout, 0); - } - inflateTemplate(template); // Set the background from XML, either directly or built from a bitmap tile final Drawable background = @@ -174,52 +157,19 @@ public class SetupWizardLayout extends FrameLayout { } @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { - mContainer.addView(child, index, params); - } - - private void addViewInternal(View child) { - super.addView(child, -1, generateDefaultLayoutParams()); - } - - private void inflateTemplate(int templateResource) { - final LayoutInflater inflater = LayoutInflater.from(getContext()); - final View templateRoot = onInflateTemplate(inflater, templateResource); - addViewInternal(templateRoot); - - mContainer = (ViewGroup) findViewById(getContainerId()); - onTemplateInflated(); - } - - /** - * This method inflates the template. Subclasses can override this method to customize the - * template inflation, or change to a different default template. The root of the inflated - * layout should be returned, and not added to the view hierarchy. - * - * @param inflater A LayoutInflater to inflate the template. - * @param template The resource ID of the template to be inflated, or 0 if no template is - * specified. - * @return Root of the inflated layout. - */ protected View onInflateTemplate(LayoutInflater inflater, int template) { if (template == 0) { template = R.layout.suw_template; } - return inflater.inflate(template, this, false); + return super.onInflateTemplate(inflater, template); } - /** - * This is called after the template has been inflated and added to the view hierarchy. - * Subclasses can implement this method to modify the template as necessary, such as caching - * views retrieved from findViewById, or other view operations that need to be done in code. - * You can think of this as {@link android.view.View#onFinishInflate()} but for inflation of the - * template instead of for child views. - */ - protected void onTemplateInflated() { - } - - protected int getContainerId() { - return R.id.suw_layout_content; + @Override + protected ViewGroup findContainer(int containerId) { + if (containerId == 0) { + containerId = R.id.suw_layout_content; + } + return super.findContainer(containerId); } public NavigationBar getNavigationBar() { @@ -426,53 +376,6 @@ public class SetupWizardLayout extends FrameLayout { } } - /* Animator support */ - - private float mXFraction; - private ViewTreeObserver.OnPreDrawListener mPreDrawListener; - - /** - * Set the X translation as a fraction of the width of this view. Make sure this method is not - * stripped out by proguard when using ObjectAnimator. You may need to add - * -keep @com.android.setupwizardlib.annotations.Keep class * - * to your proguard configuration if you are seeing mysterious MethodNotFoundExceptions at - * runtime. - */ - @Keep - public void setXFraction(float fraction) { - mXFraction = fraction; - final int width = getWidth(); - if (width != 0) { - setTranslationX(width * fraction); - } else { - // If we haven't done a layout pass yet, wait for one and then set the fraction before - // the draw occurs using an OnPreDrawListener. Don't call translationX until we know - // getWidth() has a reliable, non-zero value or else we will see the fragment flicker on - // screen. - if (mPreDrawListener == null) { - mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); - setXFraction(mXFraction); - return true; - } - }; - getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); - } - } - } - - /** - * Return the X translation as a fraction of the width, as previously set in setXFraction. - * - * @see #setXFraction(float) - */ - @Keep - public float getXFraction() { - return mXFraction; - } - /* Misc */ protected static class SavedState extends BaseSavedState { diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java index 7165f39..36dcdea 100644 --- a/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java +++ b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java @@ -22,6 +22,7 @@ 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.ListAdapter; import android.widget.ListView; @@ -31,11 +32,15 @@ public class SetupWizardListLayout extends SetupWizardLayout { private ListView mListView; public SetupWizardListLayout(Context context) { - super(context); + this(context, 0, 0); } public SetupWizardListLayout(Context context, int template) { - super(context, template); + this(context, template, 0); + } + + public SetupWizardListLayout(Context context, int template, int containerId) { + super(context, template, containerId); } public SetupWizardListLayout(Context context, AttributeSet attrs) { @@ -47,18 +52,20 @@ public class SetupWizardListLayout extends SetupWizardLayout { super(context, attrs, defStyleAttr); } - @TargetApi(VERSION_CODES.HONEYCOMB) - public SetupWizardListLayout(Context context, int template, AttributeSet attrs, - int defStyleAttr) { - super(context, template, attrs, defStyleAttr); - } - @Override protected View onInflateTemplate(LayoutInflater inflater, int template) { if (template == 0) { template = R.layout.suw_list_template; } - return inflater.inflate(template, this, false); + return super.onInflateTemplate(inflater, template); + } + + @Override + protected ViewGroup findContainer(int containerId) { + if (containerId == 0) { + containerId = android.R.id.list; + } + return super.findContainer(containerId); } @Override @@ -66,11 +73,6 @@ public class SetupWizardListLayout extends SetupWizardLayout { mListView = (ListView) findViewById(android.R.id.list); } - @Override - protected int getContainerId() { - return android.R.id.list; - } - public ListView getListView() { return mListView; } diff --git a/library/main/src/com/android/setupwizardlib/TemplateLayout.java b/library/main/src/com/android/setupwizardlib/TemplateLayout.java new file mode 100644 index 0000000..1596674 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/TemplateLayout.java @@ -0,0 +1,193 @@ +/* + * 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; +import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; + +import com.android.setupwizardlib.annotations.Keep; + +/** + * A generic template class that inflates a template, provided in the constructor or in + * android:layout through XML, and adds its children to a "container" in the template. When + * inflating this layout from XML, the {@code android:layout} and {@code suwContainer} attributes + * are required. + */ +public class TemplateLayout extends FrameLayout { + + /** + * The container of the actual content. This will be a view in the template, which child views + * will be added to when {@link #addView(View)} is called. + */ + private ViewGroup mContainer; + + public TemplateLayout(Context context, int template, int containerId) { + super(context); + init(template, containerId, null, R.attr.suwLayoutTheme); + } + + public TemplateLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(0, 0, attrs, R.attr.suwLayoutTheme); + } + + @TargetApi(VERSION_CODES.HONEYCOMB) + public TemplateLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(0, 0, attrs, defStyleAttr); + } + + // All the constructors delegate to this init method. The 3-argument constructor is not + // available in LinearLayout before v11, so call super with the exact same arguments. + private void init(int template, int containerId, AttributeSet attrs, int defStyleAttr) { + final TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.SuwTemplateLayout, defStyleAttr, 0); + if (template == 0) { + template = a.getResourceId(R.styleable.SuwTemplateLayout_android_layout, 0); + } + if (containerId == 0) { + containerId = a.getResourceId(R.styleable.SuwTemplateLayout_suwContainer, 0); + } + inflateTemplate(template, containerId); + + a.recycle(); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + mContainer.addView(child, index, params); + } + + private void addViewInternal(View child) { + super.addView(child, -1, generateDefaultLayoutParams()); + } + + private void inflateTemplate(int templateResource, int containerId) { + final LayoutInflater inflater = LayoutInflater.from(getContext()); + final View templateRoot = onInflateTemplate(inflater, templateResource); + addViewInternal(templateRoot); + + mContainer = findContainer(containerId); + if (mContainer == null) { + throw new IllegalArgumentException("Container cannot be null in TemplateLayout"); + } + onTemplateInflated(); + } + + /** + * This method inflates the template. Subclasses can override this method to customize the + * template inflation, or change to a different default template. The root of the inflated + * layout should be returned, and not added to the view hierarchy. + * + * @param inflater A LayoutInflater to inflate the template. + * @param template The resource ID of the template to be inflated, or 0 if no template is + * specified. + * @return Root of the inflated layout. + */ + protected View onInflateTemplate(LayoutInflater inflater, int template) { + if (template == 0) { + throw new IllegalArgumentException("android:layout not specified for TemplateLayout"); + } + return inflater.inflate(template, this, false); + } + + protected ViewGroup findContainer(int containerId) { + if (containerId == 0) { + // Maintain compatibility with the deprecated way of specifying container ID. + containerId = getContainerId(); + } + return (ViewGroup) findViewById(containerId); + } + + /** + * This is called after the template has been inflated and added to the view hierarchy. + * Subclasses can implement this method to modify the template as necessary, such as caching + * views retrieved from findViewById, or other view operations that need to be done in code. + * You can think of this as {@link View#onFinishInflate()} but for inflation of the + * template instead of for child views. + */ + protected void onTemplateInflated() { + } + + /** + * @return ID of the default container for this layout. This will be used to find the container + * ViewGroup, which all children views of this layout will be placed in. + * @deprecated Use the constructor with containerId argument instead. + */ + @Deprecated + protected int getContainerId() { + return 0; + } + + /* Animator support */ + + private float mXFraction; + private ViewTreeObserver.OnPreDrawListener mPreDrawListener; + + /** + * Set the X translation as a fraction of the width of this view. Make sure this method is not + * stripped out by proguard when using ObjectAnimator. You may need to add + * -keep @com.android.setupwizardlib.annotations.Keep class * + * to your proguard configuration if you are seeing mysterious MethodNotFoundExceptions at + * runtime. + */ + @Keep + @TargetApi(VERSION_CODES.HONEYCOMB) + public void setXFraction(float fraction) { + mXFraction = fraction; + final int width = getWidth(); + if (width != 0) { + setTranslationX(width * fraction); + } else { + // If we haven't done a layout pass yet, wait for one and then set the fraction before + // the draw occurs using an OnPreDrawListener. Don't call translationX until we know + // getWidth() has a reliable, non-zero value or else we will see the fragment flicker on + // screen. + if (mPreDrawListener == null) { + mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); + setXFraction(mXFraction); + return true; + } + }; + getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + } + } + + /** + * Return the X translation as a fraction of the width, as previously set in setXFraction. + * + * @see #setXFraction(float) + */ + @Keep + @TargetApi(VERSION_CODES.HONEYCOMB) + public float getXFraction() { + return mXFraction; + } +} diff --git a/library/test/res/layout/test_template_layout.xml b/library/test/res/layout/test_template_layout.xml new file mode 100644 index 0000000..9346086 --- /dev/null +++ b/library/test/res/layout/test_template_layout.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/library/test/src/com/android/setupwizardlib/test/TemplateLayoutTest.java b/library/test/src/com/android/setupwizardlib/test/TemplateLayoutTest.java new file mode 100644 index 0000000..e9379f7 --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/TemplateLayoutTest.java @@ -0,0 +1,84 @@ +/* + * 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.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import com.android.setupwizardlib.TemplateLayout; + +public class TemplateLayoutTest extends InstrumentationTestCase { + + private Context mContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mContext = getInstrumentation().getContext(); + } + + @SmallTest + public void testAddView() { + TemplateLayout layout = new TemplateLayout(mContext, R.layout.test_template, + R.id.suw_layout_content); + TextView tv = new TextView(mContext); + tv.setId(R.id.test_view_id); + layout.addView(tv); + View view = layout.findViewById(R.id.test_view_id); + assertSame("The view added should be the same text view", tv, view); + } + + @SmallTest + public void testInflateFromXml() { + LayoutInflater inflater = LayoutInflater.from(mContext); + TemplateLayout layout = + (TemplateLayout) inflater.inflate(R.layout.test_template_layout, null); + View content = layout.findViewById(R.id.test_content); + assertTrue("@id/test_content should be a TextView", content instanceof TextView); + } + + @SmallTest + public void testTemplate() { + TemplateLayout layout = new TemplateLayout(mContext, R.layout.test_template, + R.id.suw_layout_content); + View templateView = layout.findViewById(R.id.test_template_view); + assertNotNull("@id/test_template_view should exist in template", templateView); + + TextView tv = new TextView(mContext); + tv.setId(R.id.test_view_id); + layout.addView(tv); + + templateView = layout.findViewById(R.id.test_template_view); + assertNotNull("@id/test_template_view should exist in template", templateView); + View contentView = layout.findViewById(R.id.test_view_id); + assertSame("The view added should be the same text view", tv, contentView); + } + + @SmallTest + public void testNoTemplate() { + try { + new TemplateLayout(mContext, 0, 0); + fail("Inflating TemplateLayout without template should throw exception"); + } catch (IllegalArgumentException e) { + // Expected IllegalArgumentException + } + } +}