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.google.android.setupcompat.internal;
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 androidx.annotation.Keep;
24 import androidx.annotation.LayoutRes;
25 import androidx.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 import com.google.android.setupcompat.R;
33 import com.google.android.setupcompat.template.Mixin;
34 import java.util.HashMap;
35 import java.util.Map;
36 
37 /**
38  * A generic template class that inflates a template, provided in the constructor or in {@code
39  * android:layout} through XML, and adds its children to a "container" in the template. When
40  * inflating this layout from XML, the {@code android:layout} and {@code suwContainer} attributes
41  * are required.
42  *
43  * <p>This class is designed to use inside the library; it is not suitable for external use.
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 container;
52 
53   private final Map<Class<? extends Mixin>, Mixin> mixins = 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.sucLayoutTheme);
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.sucLayoutTheme);
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 =
75         getContext().obtainStyledAttributes(attrs, R.styleable.SucTemplateLayout, defStyleAttr, 0);
76     if (template == 0) {
77       template = a.getResourceId(R.styleable.SucTemplateLayout_android_layout, 0);
78     }
79     if (containerId == 0) {
80       containerId = a.getResourceId(R.styleable.SucTemplateLayout_sucContainer, 0);
81     }
82     onBeforeTemplateInflated(attrs, defStyleAttr);
83     inflateTemplate(template, containerId);
84 
85     a.recycle();
86   }
87 
88   /**
89    * Registers a mixin with a given class. This method should be called in the constructor.
90    *
91    * @param cls The class to register the mixin. In most cases, {@code cls} is the same as {@code
92    *     mixin.getClass()}, but {@code cls} can also be a super class of that. In the latter case
93    *     the mixin must be retrieved using {@code cls} in {@link #getMixin(Class)}, not the
94    *     subclass.
95    * @param mixin The mixin to be registered.
96    * @param <M> The class of the mixin to register. This is the same as {@code cls}
97    */
registerMixin(Class<M> cls, M mixin)98   protected <M extends Mixin> void registerMixin(Class<M> cls, M mixin) {
99     mixins.put(cls, mixin);
100   }
101 
102   /**
103    * Same as {@link View#findViewById(int)}, but may include views that are managed by this view but
104    * not currently added to the view hierarchy. e.g. recycler view or list view headers that are not
105    * currently shown.
106    */
107   // Returning generic type is the common pattern used for findViewBy* methods
108   @SuppressWarnings("TypeParameterUnusedInFormals")
findManagedViewById(int id)109   public <T extends View> T findManagedViewById(int id) {
110     return findViewById(id);
111   }
112 
113   /**
114    * Get a {@link Mixin} from this template registered earlier in {@link #registerMixin(Class,
115    * Mixin)}.
116    *
117    * @param cls The class marker of Mixin being requested. The actual Mixin returned may be a
118    *     subclass of this marker. Note that this must be the same class as registered in {@link
119    *     #registerMixin(Class, Mixin)}, which is not necessarily the same as the concrete class of
120    *     the instance returned by this method.
121    * @param <M> The type of the class marker.
122    * @return The mixin marked by {@code cls}, or null if the template does not have a matching
123    *     mixin.
124    */
125   @SuppressWarnings("unchecked")
getMixin(Class<M> cls)126   public <M extends Mixin> M getMixin(Class<M> cls) {
127     return (M) mixins.get(cls);
128   }
129 
130   @Override
addView(View child, int index, ViewGroup.LayoutParams params)131   public void addView(View child, int index, ViewGroup.LayoutParams params) {
132     container.addView(child, index, params);
133   }
134 
addViewInternal(View child)135   private void addViewInternal(View child) {
136     super.addView(child, -1, generateDefaultLayoutParams());
137   }
138 
inflateTemplate(int templateResource, int containerId)139   private void inflateTemplate(int templateResource, int containerId) {
140     final LayoutInflater inflater = LayoutInflater.from(getContext());
141     final View templateRoot = onInflateTemplate(inflater, templateResource);
142     addViewInternal(templateRoot);
143 
144     container = findContainer(containerId);
145     if (container == null) {
146       throw new IllegalArgumentException("Container cannot be null in TemplateLayout");
147     }
148     onTemplateInflated();
149   }
150 
151   /**
152    * Inflate the template using the given inflater and theme. The fallback theme will be applied to
153    * the theme without overriding the values already defined in the theme, but simply providing
154    * default values for values which have not been defined. This allows templates to add additional
155    * required theme attributes without breaking existing clients.
156    *
157    * <p>In general, clients should still set the activity theme to the corresponding theme in setup
158    * wizard lib, so that the content area gets the correct styles as well.
159    *
160    * @param inflater A LayoutInflater to inflate the template.
161    * @param fallbackTheme A fallback theme to apply to the template. If the values defined in the
162    *     fallback theme is already defined in the original theme, the value in the original theme
163    *     takes precedence.
164    * @param template The layout template to be inflated.
165    * @return Root of the inflated layout.
166    * @see FallbackThemeWrapper
167    */
inflateTemplate( LayoutInflater inflater, @StyleRes int fallbackTheme, @LayoutRes int template)168   protected final View inflateTemplate(
169       LayoutInflater inflater, @StyleRes int fallbackTheme, @LayoutRes int template) {
170     if (template == 0) {
171       throw new IllegalArgumentException("android:layout not specified for TemplateLayout");
172     }
173     if (fallbackTheme != 0) {
174       inflater =
175           LayoutInflater.from(new FallbackThemeWrapper(inflater.getContext(), fallbackTheme));
176     }
177     return inflater.inflate(template, this, false);
178   }
179 
180   /**
181    * This method inflates the template. Subclasses can override this method to customize the
182    * template inflation, or change to a different default template. The root of the inflated layout
183    * should be returned, and not added to the view hierarchy.
184    *
185    * @param inflater A LayoutInflater to inflate the template.
186    * @param template The resource ID of the template to be inflated, or 0 if no template is
187    *     specified.
188    * @return Root of the inflated layout.
189    */
onInflateTemplate(LayoutInflater inflater, @LayoutRes int template)190   protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) {
191     return inflateTemplate(inflater, 0, template);
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. Subclasses
204    * can implement this method to modify the template as necessary, such as caching views retrieved
205    * from findViewById, or other view operations that need to be done in code. You can think of this
206    * as {@link View#onFinishInflate()} but for inflation of the template instead of for child views.
207    */
onTemplateInflated()208   protected void onTemplateInflated() {}
209 
210   /**
211    * This is called before the template has been inflated and added to the view hierarchy.
212    * Subclasses can implement this method to modify the template as necessary, such as something
213    * need to be done before onTemplateInflated which is called while still in the constructor.
214    */
onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr)215   protected void onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr) {}
216 
217   /**
218    * @return ID of the default container for this layout. This will be used to find the container
219    *     ViewGroup, which all children views of this layout will be placed in.
220    * @deprecated Override {@link #findContainer(int)} instead.
221    */
222   @Deprecated
getContainerId()223   protected int getContainerId() {
224     return 0;
225   }
226 
227   /* Animator support */
228 
229   private float xFraction;
230   private ViewTreeObserver.OnPreDrawListener preDrawListener;
231 
232   /**
233    * Set the X translation as a fraction of the width of this view. Make sure this method is not
234    * stripped out by proguard when using this with {@link android.animation.ObjectAnimator}. You may
235    * need to add <code>
236    *     -keep @androidx.annotation.Keep class *
237    * </code> to your proguard configuration if you are seeing mysterious {@link NoSuchMethodError}
238    * at runtime.
239    */
240   @Keep
241   @TargetApi(VERSION_CODES.HONEYCOMB)
setXFraction(float fraction)242   public void setXFraction(float fraction) {
243     xFraction = fraction;
244     final int width = getWidth();
245     if (width != 0) {
246       setTranslationX(width * fraction);
247     } else {
248       // If we haven't done a layout pass yet, wait for one and then set the fraction before
249       // the draw occurs using an OnPreDrawListener. Don't call translationX until we know
250       // getWidth() has a reliable, non-zero value or else we will see the fragment flicker on
251       // screen.
252       if (preDrawListener == null) {
253         preDrawListener =
254             new ViewTreeObserver.OnPreDrawListener() {
255               @Override
256               public boolean onPreDraw() {
257                 getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
258                 setXFraction(xFraction);
259                 return true;
260               }
261             };
262         getViewTreeObserver().addOnPreDrawListener(preDrawListener);
263       }
264     }
265   }
266 
267   /**
268    * Return the X translation as a fraction of the width, as previously set in {@link
269    * #setXFraction(float)}.
270    *
271    * @see #setXFraction(float)
272    */
273   @Keep
274   @TargetApi(VERSION_CODES.HONEYCOMB)
getXFraction()275   public float getXFraction() {
276     return xFraction;
277   }
278 }
279