summaryrefslogtreecommitdiff
path: root/main/java/com/google/android/setupcompat/internal/TemplateLayout.java
blob: 34179d64f8da6d8df1516255445ea86930052734 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/*
 * 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.google.android.setupcompat.internal;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build.VERSION_CODES;
import androidx.annotation.Keep;
import androidx.annotation.LayoutRes;
import androidx.annotation.StyleRes;
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.google.android.setupcompat.R;
import com.google.android.setupcompat.template.Mixin;
import java.util.HashMap;
import java.util.Map;

/**
 * A generic template class that inflates a template, provided in the constructor or in {@code
 * 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.
 *
 * <p>This class is designed to use inside the library; it is not suitable for external use.
 */
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 container;

  private final Map<Class<? extends Mixin>, Mixin> mixins = new HashMap<>();

  public TemplateLayout(Context context, int template, int containerId) {
    super(context);
    init(template, containerId, null, R.attr.sucLayoutTheme);
  }

  public TemplateLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(0, 0, attrs, R.attr.sucLayoutTheme);
  }

  @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.SucTemplateLayout, defStyleAttr, 0);
    if (template == 0) {
      template = a.getResourceId(R.styleable.SucTemplateLayout_android_layout, 0);
    }
    if (containerId == 0) {
      containerId = a.getResourceId(R.styleable.SucTemplateLayout_sucContainer, 0);
    }
    onBeforeTemplateInflated(attrs, defStyleAttr);
    inflateTemplate(template, containerId);

    a.recycle();
  }

  /**
   * Registers a mixin with a given class. This method should be called in the constructor.
   *
   * @param cls The class to register the mixin. In most cases, {@code cls} is the same as {@code
   *     mixin.getClass()}, but {@code cls} can also be a super class of that. In the latter case
   *     the mixin must be retrieved using {@code cls} in {@link #getMixin(Class)}, not the
   *     subclass.
   * @param mixin The mixin to be registered.
   * @param <M> The class of the mixin to register. This is the same as {@code cls}
   */
  protected <M extends Mixin> void registerMixin(Class<M> cls, M mixin) {
    mixins.put(cls, mixin);
  }

  /**
   * Same as {@link View#findViewById(int)}, but may include views that are managed by this view but
   * not currently added to the view hierarchy. e.g. recycler view or list view headers that are not
   * currently shown.
   */
  // Returning generic type is the common pattern used for findViewBy* methods
  @SuppressWarnings("TypeParameterUnusedInFormals")
  public <T extends View> T findManagedViewById(int id) {
    return findViewById(id);
  }

  /**
   * Get a {@link Mixin} from this template registered earlier in {@link #registerMixin(Class,
   * Mixin)}.
   *
   * @param cls The class marker of Mixin being requested. The actual Mixin returned may be a
   *     subclass of this marker. Note that this must be the same class as registered in {@link
   *     #registerMixin(Class, Mixin)}, which is not necessarily the same as the concrete class of
   *     the instance returned by this method.
   * @param <M> The type of the class marker.
   * @return The mixin marked by {@code cls}, or null if the template does not have a matching
   *     mixin.
   */
  @SuppressWarnings("unchecked")
  public <M extends Mixin> M getMixin(Class<M> cls) {
    return (M) mixins.get(cls);
  }

  @Override
  public void addView(View child, int index, ViewGroup.LayoutParams params) {
    container.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);

    container = findContainer(containerId);
    if (container == null) {
      throw new IllegalArgumentException("Container cannot be null in TemplateLayout");
    }
    onTemplateInflated();
  }

  /**
   * Inflate the template using the given inflater and theme. The fallback theme will be applied to
   * the theme without overriding the values already defined in the theme, but simply providing
   * default values for values which have not been defined. This allows templates to add additional
   * required theme attributes without breaking existing clients.
   *
   * <p>In general, clients should still set the activity theme to the corresponding theme in setup
   * wizard lib, so that the content area gets the correct styles as well.
   *
   * @param inflater A LayoutInflater to inflate the template.
   * @param fallbackTheme A fallback theme to apply to the template. If the values defined in the
   *     fallback theme is already defined in the original theme, the value in the original theme
   *     takes precedence.
   * @param template The layout template to be inflated.
   * @return Root of the inflated layout.
   * @see FallbackThemeWrapper
   */
  protected final View inflateTemplate(
      LayoutInflater inflater, @StyleRes int fallbackTheme, @LayoutRes int template) {
    if (template == 0) {
      throw new IllegalArgumentException("android:layout not specified for TemplateLayout");
    }
    if (fallbackTheme != 0) {
      inflater =
          LayoutInflater.from(new FallbackThemeWrapper(inflater.getContext(), fallbackTheme));
    }
    return inflater.inflate(template, this, false);
  }

  /**
   * 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, @LayoutRes int template) {
    return inflateTemplate(inflater, 0, template);
  }

  protected ViewGroup findContainer(int containerId) {
    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() {}

  /**
   * This is called before the template has been inflated and added to the view hierarchy.
   * Subclasses can implement this method to modify the template as necessary, such as something
   * need to be done before onTemplateInflated which is called while still in the constructor.
   */
  protected void onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr) {}

  /* Animator support */

  private float xFraction;
  private ViewTreeObserver.OnPreDrawListener preDrawListener;

  /**
   * 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 this with {@link android.animation.ObjectAnimator}. You may
   * need to add <code>
   *     -keep @androidx.annotation.Keep class *
   * </code> to your proguard configuration if you are seeing mysterious {@link NoSuchMethodError}
   * at runtime.
   */
  @Keep
  @TargetApi(VERSION_CODES.HONEYCOMB)
  public void setXFraction(float fraction) {
    xFraction = 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 (preDrawListener == null) {
        preDrawListener =
            new ViewTreeObserver.OnPreDrawListener() {
              @Override
              public boolean onPreDraw() {
                getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
                setXFraction(xFraction);
                return true;
              }
            };
        getViewTreeObserver().addOnPreDrawListener(preDrawListener);
      }
    }
  }

  /**
   * Return the X translation as a fraction of the width, as previously set in {@link
   * #setXFraction(float)}.
   *
   * @see #setXFraction(float)
   */
  @Keep
  @TargetApi(VERSION_CODES.HONEYCOMB)
  public float getXFraction() {
    return xFraction;
  }
}