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 17package com.android.setupwizardlib; 18 19import android.annotation.TargetApi; 20import android.content.Context; 21import android.content.res.TypedArray; 22import android.os.Build.VERSION_CODES; 23import android.support.annotation.Keep; 24import android.support.annotation.LayoutRes; 25import android.support.annotation.StyleRes; 26import android.util.AttributeSet; 27import android.view.LayoutInflater; 28import android.view.View; 29import android.view.ViewGroup; 30import android.view.ViewTreeObserver; 31import android.widget.FrameLayout; 32 33import com.android.setupwizardlib.template.Mixin; 34import com.android.setupwizardlib.util.FallbackThemeWrapper; 35 36import java.util.HashMap; 37import 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 */ 45public 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 55 public TemplateLayout(Context context, int template, int containerId) { 56 super(context); 57 init(template, containerId, null, R.attr.suwLayoutTheme); 58 } 59 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) 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 = 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 */ 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") 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") 125 public <M extends Mixin> M getMixin(Class<M> cls) { 126 return (M) mMixins.get(cls); 127 } 128 129 @Override 130 public void addView(View child, int index, ViewGroup.LayoutParams params) { 131 mContainer.addView(child, index, params); 132 } 133 134 private void addViewInternal(View child) { 135 super.addView(child, -1, generateDefaultLayoutParams()); 136 } 137 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 */ 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 */ 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 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 */ 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 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) 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) 271 public float getXFraction() { 272 return mXFraction; 273 } 274} 275