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.android.setupwizardlib; 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 android.support.annotation.Keep; 24 import android.support.annotation.LayoutRes; 25 import android.support.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 33 import com.android.setupwizardlib.template.Mixin; 34 import com.android.setupwizardlib.util.FallbackThemeWrapper; 35 36 import java.util.HashMap; 37 import 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 */ 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 mContainer; 52 53 private Map<Class<? extends Mixin>, Mixin> mMixins = 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.suwLayoutTheme); 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.suwLayoutTheme); 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 = 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 */ registerMixin(Class<M> cls, M mixin)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 */ findManagedViewById(int id)106 public View findManagedViewById(int id) { 107 return findViewById(id); 108 } 109 110 /** 111 * Get a {@link Mixin} from this template registered earlier in 112 * {@link #registerMixin(Class, Mixin)}. 113 * 114 * @param cls The class marker of Mixin being requested. The actual Mixin returned may be a 115 * subclass of this marker. Note that this must be the same class as registered in 116 * {@link #registerMixin(Class, Mixin)}, which is not necessarily the 117 * same as the concrete class of the instance returned by this method. 118 * @param <M> The type of the class marker. 119 * @return The mixin marked by {@code cls}, or null if the template does not have a matching 120 * mixin. 121 */ 122 @SuppressWarnings("unchecked") getMixin(Class<M> cls)123 public <M extends Mixin> M getMixin(Class<M> cls) { 124 return (M) mMixins.get(cls); 125 } 126 127 @Override addView(View child, int index, ViewGroup.LayoutParams params)128 public void addView(View child, int index, ViewGroup.LayoutParams params) { 129 mContainer.addView(child, index, params); 130 } 131 addViewInternal(View child)132 private void addViewInternal(View child) { 133 super.addView(child, -1, generateDefaultLayoutParams()); 134 } 135 inflateTemplate(int templateResource, int containerId)136 private void inflateTemplate(int templateResource, int containerId) { 137 final LayoutInflater inflater = LayoutInflater.from(getContext()); 138 final View templateRoot = onInflateTemplate(inflater, templateResource); 139 addViewInternal(templateRoot); 140 141 mContainer = findContainer(containerId); 142 if (mContainer == null) { 143 throw new IllegalArgumentException("Container cannot be null in TemplateLayout"); 144 } 145 onTemplateInflated(); 146 } 147 148 /** 149 * This method inflates the template. Subclasses can override this method to customize the 150 * template inflation, or change to a different default template. The root of the inflated 151 * layout should be returned, and not added to the view hierarchy. 152 * 153 * @param inflater A LayoutInflater to inflate the template. 154 * @param template The resource ID of the template to be inflated, or 0 if no template is 155 * specified. 156 * @return Root of the inflated layout. 157 */ onInflateTemplate(LayoutInflater inflater, @LayoutRes int template)158 protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) { 159 return inflateTemplate(inflater, 0, template); 160 } 161 162 /** 163 * Inflate the template using the given inflater and theme. The fallback theme will be applied 164 * to the theme without overriding the values already defined in the theme, but simply providing 165 * default values for values which have not been defined. This allows templates to add 166 * additional required theme attributes without breaking existing clients. 167 * 168 * <p>In general, clients should still set the activity theme to the corresponding theme in 169 * setup wizard lib, so that the content area gets the correct styles as well. 170 * 171 * @param inflater A LayoutInflater to inflate the template. 172 * @param fallbackTheme A fallback theme to apply to the template. If the values defined in the 173 * fallback theme is already defined in the original theme, the value in 174 * the original theme takes precedence. 175 * @param template The layout template to be inflated. 176 * @return Root of the inflated layout. 177 * 178 * @see FallbackThemeWrapper 179 */ inflateTemplate(LayoutInflater inflater, @StyleRes int fallbackTheme, @LayoutRes int template)180 protected final View inflateTemplate(LayoutInflater inflater, @StyleRes int fallbackTheme, 181 @LayoutRes int template) { 182 if (template == 0) { 183 throw new IllegalArgumentException("android:layout not specified for TemplateLayout"); 184 } 185 if (fallbackTheme != 0) { 186 inflater = LayoutInflater.from( 187 new FallbackThemeWrapper(inflater.getContext(), fallbackTheme)); 188 } 189 return inflater.inflate(template, this, false); 190 } 191 findContainer(int containerId)192 protected ViewGroup findContainer(int containerId) { 193 if (containerId == 0) { 194 // Maintain compatibility with the deprecated way of specifying container ID. 195 containerId = getContainerId(); 196 } 197 return (ViewGroup) findViewById(containerId); 198 } 199 200 /** 201 * This is called after the template has been inflated and added to the view hierarchy. 202 * Subclasses can implement this method to modify the template as necessary, such as caching 203 * views retrieved from findViewById, or other view operations that need to be done in code. 204 * You can think of this as {@link View#onFinishInflate()} but for inflation of the 205 * template instead of for child views. 206 */ onTemplateInflated()207 protected void onTemplateInflated() { 208 } 209 210 /** 211 * @return ID of the default container for this layout. This will be used to find the container 212 * ViewGroup, which all children views of this layout will be placed in. 213 * @deprecated Override {@link #findContainer(int)} instead. 214 */ 215 @Deprecated getContainerId()216 protected int getContainerId() { 217 return 0; 218 } 219 220 /* Animator support */ 221 222 private float mXFraction; 223 private ViewTreeObserver.OnPreDrawListener mPreDrawListener; 224 225 /** 226 * Set the X translation as a fraction of the width of this view. Make sure this method is not 227 * stripped out by proguard when using this with {@link android.animation.ObjectAnimator}. You 228 * may need to add 229 * <code> 230 * -keep @android.support.annotation.Keep class * 231 * </code> 232 * to your proguard configuration if you are seeing mysterious {@link NoSuchMethodError} at 233 * runtime. 234 */ 235 @Keep 236 @TargetApi(VERSION_CODES.HONEYCOMB) setXFraction(float fraction)237 public void setXFraction(float fraction) { 238 mXFraction = fraction; 239 final int width = getWidth(); 240 if (width != 0) { 241 setTranslationX(width * fraction); 242 } else { 243 // If we haven't done a layout pass yet, wait for one and then set the fraction before 244 // the draw occurs using an OnPreDrawListener. Don't call translationX until we know 245 // getWidth() has a reliable, non-zero value or else we will see the fragment flicker on 246 // screen. 247 if (mPreDrawListener == null) { 248 mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { 249 @Override 250 public boolean onPreDraw() { 251 getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); 252 setXFraction(mXFraction); 253 return true; 254 } 255 }; 256 getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); 257 } 258 } 259 } 260 261 /** 262 * Return the X translation as a fraction of the width, as previously set in 263 * {@link #setXFraction(float)}. 264 * 265 * @see #setXFraction(float) 266 */ 267 @Keep 268 @TargetApi(VERSION_CODES.HONEYCOMB) getXFraction()269 public float getXFraction() { 270 return mXFraction; 271 } 272 } 273