[SuwLib] Refactor out TemplateLayout

Refactor TemplateLayout as a base class for SetupWizardLayout, which
is a generic layout that takes the "android:layout", inflates that as
a template and put its children in the "container".

Change-Id: Id7977787ffa6cdb5df7a4cb8172ce1fa6a52ed45
This commit is contained in:
Maurice Lam 2015-08-13 12:56:28 -07:00
parent e2eb715ed2
commit bdfc0132ff
6 changed files with 347 additions and 131 deletions

View file

@ -37,7 +37,6 @@
</declare-styleable>
<declare-styleable name="SuwSetupWizardLayout">
<attr name="android:layout" />
<attr name="suwBackground" format="color|reference" />
<attr name="suwBackgroundTile" format="color|reference" />
<attr name="suwHeaderText" format="string" localization="suggested" />
@ -48,4 +47,9 @@
<attr name="suwIllustrationImage" format="color|reference" />
</declare-styleable>
<declare-styleable name="SuwTemplateLayout">
<attr name="android:layout" />
<attr name="suwContainer" format="reference" />
</declare-styleable>
</resources>

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -0,0 +1,30 @@
<!--
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.TemplateLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/test_template"
app:suwContainer="@+id/suw_layout_content">
<TextView
android:id="@+id/test_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.android.setupwizardlib.TemplateLayout>

View file

@ -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
}
}
}