1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.setupwizardlib;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.os.Build.VERSION_CODES;
23 import android.support.annotation.Keep;
24 import android.support.annotation.LayoutRes;
25 import android.support.annotation.StyleRes;
26 import android.util.AttributeSet;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.ViewTreeObserver;
31 import android.widget.FrameLayout;
32 
33 import com.android.setupwizardlib.template.Mixin;
34 import com.android.setupwizardlib.util.FallbackThemeWrapper;
35 
36 import java.util.HashMap;
37 import java.util.Map;
38 
39 /**
40  * A generic template class that inflates a template, provided in the constructor or in
41  * {@code android:layout} through XML, and adds its children to a "container" in the template. When
42  * inflating this layout from XML, the {@code android:layout} and {@code suwContainer} attributes
43  * are required.
44  */
45 public class TemplateLayout extends FrameLayout {
46 
47     /**
48      * The container of the actual content. This will be a view in the template, which child views
49      * will be added to when {@link #addView(View)} is called.
50      */
51     private ViewGroup mContainer;
52 
53     private Map<Class<? extends Mixin>, Mixin> mMixins = new HashMap<>();
54 
TemplateLayout(Context context, int template, int containerId)55     public TemplateLayout(Context context, int template, int containerId) {
56         super(context);
57         init(template, containerId, null, R.attr.suwLayoutTheme);
58     }
59 
TemplateLayout(Context context, AttributeSet attrs)60     public TemplateLayout(Context context, AttributeSet attrs) {
61         super(context, attrs);
62         init(0, 0, attrs, R.attr.suwLayoutTheme);
63     }
64 
65     @TargetApi(VERSION_CODES.HONEYCOMB)
TemplateLayout(Context context, AttributeSet attrs, int defStyleAttr)66     public TemplateLayout(Context context, AttributeSet attrs, int defStyleAttr) {
67         super(context, attrs, defStyleAttr);
68         init(0, 0, attrs, defStyleAttr);
69     }
70 
71     // All the constructors delegate to this init method. The 3-argument constructor is not
72     // available in LinearLayout before v11, so call super with the exact same arguments.
init(int template, int containerId, AttributeSet attrs, int defStyleAttr)73     private void init(int template, int containerId, AttributeSet attrs, int defStyleAttr) {
74         final TypedArray a = getContext().obtainStyledAttributes(attrs,
75                 R.styleable.SuwTemplateLayout, defStyleAttr, 0);
76         if (template == 0) {
77             template = a.getResourceId(R.styleable.SuwTemplateLayout_android_layout, 0);
78         }
79         if (containerId == 0) {
80             containerId = a.getResourceId(R.styleable.SuwTemplateLayout_suwContainer, 0);
81         }
82         inflateTemplate(template, containerId);
83 
84         a.recycle();
85     }
86 
87     /**
88      * Registers a mixin with a given class. This method should be called in the constructor.
89      *
90      * @param cls The class to register the mixin. In most cases, {@code cls} is the same as
91      *            {@code mixin.getClass()}, but {@code cls} can also be a super class of that. In
92      *            the latter case the the mixin must be retrieved using {@code cls} in
93      *            {@link #getMixin(Class)}, not the subclass.
94      * @param mixin The mixin to be registered.
95      * @param <M> The class of the mixin to register. This is the same as {@code cls}
96      */
registerMixin(Class<M> cls, M mixin)97     protected <M extends Mixin> void registerMixin(Class<M> cls, M mixin) {
98         mMixins.put(cls, mixin);
99     }
100 
101     /**
102      * Same as {@link android.view.View#findViewById(int)}, but may include views that are managed
103      * by this view but not currently added to the view hierarchy. e.g. recycler view or list view
104      * headers that are not currently shown.
105      */
106     // Returning generic type is the common pattern used for findViewBy* methods
107     @SuppressWarnings("TypeParameterUnusedInFormals")
findManagedViewById(int id)108     public <T extends View> T findManagedViewById(int id) {
109         return findViewById(id);
110     }
111 
112     /**
113      * Get a {@link Mixin} from this template registered earlier in
114      * {@link #registerMixin(Class, Mixin)}.
115      *
116      * @param cls The class marker of Mixin being requested. The actual Mixin returned may be a
117      *            subclass of this marker. Note that this must be the same class as registered in
118      *            {@link #registerMixin(Class, Mixin)}, which is not necessarily the
119      *            same as the concrete class of the instance returned by this method.
120      * @param <M> The type of the class marker.
121      * @return The mixin marked by {@code cls}, or null if the template does not have a matching
122      *         mixin.
123      */
124     @SuppressWarnings("unchecked")
getMixin(Class<M> cls)125     public <M extends Mixin> M getMixin(Class<M> cls) {
126         return (M) mMixins.get(cls);
127     }
128 
129     @Override
addView(View child, int index, ViewGroup.LayoutParams params)130     public void addView(View child, int index, ViewGroup.LayoutParams params) {
131         mContainer.addView(child, index, params);
132     }
133 
addViewInternal(View child)134     private void addViewInternal(View child) {
135         super.addView(child, -1, generateDefaultLayoutParams());
136     }
137 
inflateTemplate(int templateResource, int containerId)138     private void inflateTemplate(int templateResource, int containerId) {
139         final LayoutInflater inflater = LayoutInflater.from(getContext());
140         final View templateRoot = onInflateTemplate(inflater, templateResource);
141         addViewInternal(templateRoot);
142 
143         mContainer = findContainer(containerId);
144         if (mContainer == null) {
145             throw new IllegalArgumentException("Container cannot be null in TemplateLayout");
146         }
147         onTemplateInflated();
148     }
149 
150     /**
151      * This method inflates the template. Subclasses can override this method to customize the
152      * template inflation, or change to a different default template. The root of the inflated
153      * layout should be returned, and not added to the view hierarchy.
154      *
155      * @param inflater A LayoutInflater to inflate the template.
156      * @param template The resource ID of the template to be inflated, or 0 if no template is
157      *                 specified.
158      * @return Root of the inflated layout.
159      */
onInflateTemplate(LayoutInflater inflater, @LayoutRes int template)160     protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) {
161         return inflateTemplate(inflater, 0, template);
162     }
163 
164     /**
165      * Inflate the template using the given inflater and theme. The fallback theme will be applied
166      * to the theme without overriding the values already defined in the theme, but simply providing
167      * default values for values which have not been defined. This allows templates to add
168      * additional required theme attributes without breaking existing clients.
169      *
170      * <p>In general, clients should still set the activity theme to the corresponding theme in
171      * setup wizard lib, so that the content area gets the correct styles as well.
172      *
173      * @param inflater A LayoutInflater to inflate the template.
174      * @param fallbackTheme A fallback theme to apply to the template. If the values defined in the
175      *                      fallback theme is already defined in the original theme, the value in
176      *                      the original theme takes precedence.
177      * @param template The layout template to be inflated.
178      * @return Root of the inflated layout.
179      *
180      * @see FallbackThemeWrapper
181      */
inflateTemplate(LayoutInflater inflater, @StyleRes int fallbackTheme, @LayoutRes int template)182     protected final View inflateTemplate(LayoutInflater inflater, @StyleRes int fallbackTheme,
183             @LayoutRes int template) {
184         if (template == 0) {
185             throw new IllegalArgumentException("android:layout not specified for TemplateLayout");
186         }
187         if (fallbackTheme != 0) {
188             inflater = LayoutInflater.from(
189                     new FallbackThemeWrapper(inflater.getContext(), fallbackTheme));
190         }
191         return inflater.inflate(template, this, false);
192     }
193 
findContainer(int containerId)194     protected ViewGroup findContainer(int containerId) {
195         if (containerId == 0) {
196             // Maintain compatibility with the deprecated way of specifying container ID.
197             containerId = getContainerId();
198         }
199         return (ViewGroup) findViewById(containerId);
200     }
201 
202     /**
203      * This is called after the template has been inflated and added to the view hierarchy.
204      * Subclasses can implement this method to modify the template as necessary, such as caching
205      * views retrieved from findViewById, or other view operations that need to be done in code.
206      * You can think of this as {@link View#onFinishInflate()} but for inflation of the
207      * template instead of for child views.
208      */
onTemplateInflated()209     protected void onTemplateInflated() {
210     }
211 
212     /**
213      * @return ID of the default container for this layout. This will be used to find the container
214      * ViewGroup, which all children views of this layout will be placed in.
215      * @deprecated Override {@link #findContainer(int)} instead.
216      */
217     @Deprecated
getContainerId()218     protected int getContainerId() {
219         return 0;
220     }
221 
222     /* Animator support */
223 
224     private float mXFraction;
225     private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
226 
227     /**
228      * Set the X translation as a fraction of the width of this view. Make sure this method is not
229      * stripped out by proguard when using this with {@link android.animation.ObjectAnimator}. You
230      * may need to add
231      * <code>
232      *     -keep @android.support.annotation.Keep class *
233      * </code>
234      * to your proguard configuration if you are seeing mysterious {@link NoSuchMethodError} at
235      * runtime.
236      */
237     @Keep
238     @TargetApi(VERSION_CODES.HONEYCOMB)
setXFraction(float fraction)239     public void setXFraction(float fraction) {
240         mXFraction = fraction;
241         final int width = getWidth();
242         if (width != 0) {
243             setTranslationX(width * fraction);
244         } else {
245             // If we haven't done a layout pass yet, wait for one and then set the fraction before
246             // the draw occurs using an OnPreDrawListener. Don't call translationX until we know
247             // getWidth() has a reliable, non-zero value or else we will see the fragment flicker on
248             // screen.
249             if (mPreDrawListener == null) {
250                 mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
251                     @Override
252                     public boolean onPreDraw() {
253                         getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener);
254                         setXFraction(mXFraction);
255                         return true;
256                     }
257                 };
258                 getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
259             }
260         }
261     }
262 
263     /**
264      * Return the X translation as a fraction of the width, as previously set in
265      * {@link #setXFraction(float)}.
266      *
267      * @see #setXFraction(float)
268      */
269     @Keep
270     @TargetApi(VERSION_CODES.HONEYCOMB)
getXFraction()271     public float getXFraction() {
272         return mXFraction;
273     }
274 }
275