Home | History | Annotate | Download | only in internal
      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 
     55   public TemplateLayout(Context context, int template, int containerId) {
     56     super(context);
     57     init(template, containerId, null, R.attr.sucLayoutTheme);
     58   }
     59 
     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)
     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.
     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    */
     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")
    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")
    126   public <M extends Mixin> M getMixin(Class<M> cls) {
    127     return (M) mixins.get(cls);
    128   }
    129 
    130   @Override
    131   public void addView(View child, int index, ViewGroup.LayoutParams params) {
    132     container.addView(child, index, params);
    133   }
    134 
    135   private void addViewInternal(View child) {
    136     super.addView(child, -1, generateDefaultLayoutParams());
    137   }
    138 
    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    */
    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    */
    190   protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) {
    191     return inflateTemplate(inflater, 0, template);
    192   }
    193 
    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    */
    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    */
    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
    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)
    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)
    275   public float getXFraction() {
    276     return xFraction;
    277   }
    278 }
    279