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