1 /* 2 * Copyright (C) 2014 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 androidx.appcompat.view; 18 19 import android.content.Context; 20 import android.content.ContextWrapper; 21 import android.content.res.AssetManager; 22 import android.content.res.Configuration; 23 import android.content.res.Resources; 24 import android.os.Build; 25 import android.view.LayoutInflater; 26 27 import androidx.annotation.RequiresApi; 28 import androidx.annotation.StyleRes; 29 import androidx.appcompat.R; 30 31 /** 32 * A context wrapper that allows you to modify or replace the theme of the wrapped context. 33 */ 34 public class ContextThemeWrapper extends ContextWrapper { 35 /** 36 * Lazily-populated configuration object representing an empty, default configuration. 37 */ 38 private static Configuration sEmptyConfig; 39 40 private int mThemeResource; 41 private Resources.Theme mTheme; 42 private LayoutInflater mInflater; 43 private Configuration mOverrideConfiguration; 44 private Resources mResources; 45 46 /** 47 * Creates a new context wrapper with no theme and no base context. 48 * <p class="note"> 49 * <strong>Note:</strong> A base context <strong>must</strong> be attached 50 * using {@link #attachBaseContext(Context)} before calling any other 51 * method on the newly constructed context wrapper. 52 */ ContextThemeWrapper()53 public ContextThemeWrapper() { 54 super(null); 55 } 56 57 /** 58 * Creates a new context wrapper with the specified theme. 59 * <p> 60 * The specified theme will be applied on top of the base context's theme. 61 * Any attributes not explicitly defined in the theme identified by 62 * <var>themeResId</var> will retain their original values. 63 * 64 * @param base the base context 65 * @param themeResId the resource ID of the theme to be applied on top of 66 * the base context's theme 67 */ ContextThemeWrapper(Context base, @StyleRes int themeResId)68 public ContextThemeWrapper(Context base, @StyleRes int themeResId) { 69 super(base); 70 mThemeResource = themeResId; 71 } 72 73 /** 74 * Creates a new context wrapper with the specified theme. 75 * <p> 76 * Unlike {@link #ContextThemeWrapper(Context, int)}, the theme passed to 77 * this constructor will completely replace the base context's theme. 78 * 79 * @param base the base context 80 * @param theme the theme against which resources should be inflated 81 */ ContextThemeWrapper(Context base, Resources.Theme theme)82 public ContextThemeWrapper(Context base, Resources.Theme theme) { 83 super(base); 84 mTheme = theme; 85 } 86 87 @Override attachBaseContext(Context newBase)88 protected void attachBaseContext(Context newBase) { 89 super.attachBaseContext(newBase); 90 } 91 92 /** 93 * Call to set an "override configuration" on this context -- this is 94 * a configuration that replies one or more values of the standard 95 * configuration that is applied to the context. See 96 * {@link Context#createConfigurationContext(Configuration)} for more 97 * information. 98 * 99 * <p>This method can only be called once, and must be called before any 100 * calls to {@link #getResources()} or {@link #getAssets()} are made. 101 */ applyOverrideConfiguration(Configuration overrideConfiguration)102 public void applyOverrideConfiguration(Configuration overrideConfiguration) { 103 if (mResources != null) { 104 throw new IllegalStateException( 105 "getResources() or getAssets() has already been called"); 106 } 107 if (mOverrideConfiguration != null) { 108 throw new IllegalStateException("Override configuration has already been set"); 109 } 110 mOverrideConfiguration = new Configuration(overrideConfiguration); 111 } 112 113 @Override getResources()114 public Resources getResources() { 115 return getResourcesInternal(); 116 } 117 getResourcesInternal()118 private Resources getResourcesInternal() { 119 if (mResources == null) { 120 if (mOverrideConfiguration == null 121 || (Build.VERSION.SDK_INT >= 26 122 && isEmptyConfiguration(mOverrideConfiguration))) { 123 // If we're not applying any overrides, use the base context's resources. On API 124 // 26+, this will avoid pulling in resources that share a backing implementation 125 // with the application context. 126 mResources = super.getResources(); 127 } else { 128 final Context resContext = 129 createConfigurationContext(mOverrideConfiguration); 130 mResources = resContext.getResources(); 131 } 132 } 133 return mResources; 134 } 135 136 @Override setTheme(int resid)137 public void setTheme(int resid) { 138 if (mThemeResource != resid) { 139 mThemeResource = resid; 140 initializeTheme(); 141 } 142 } 143 144 /** 145 * Returns the resource ID of the theme that is to be applied on top of the base context's 146 * theme. 147 */ getThemeResId()148 public int getThemeResId() { 149 return mThemeResource; 150 } 151 152 @Override getTheme()153 public Resources.Theme getTheme() { 154 if (mTheme != null) { 155 return mTheme; 156 } 157 158 if (mThemeResource == 0) { 159 mThemeResource = R.style.Theme_AppCompat_Light; 160 } 161 initializeTheme(); 162 163 return mTheme; 164 } 165 166 @Override getSystemService(String name)167 public Object getSystemService(String name) { 168 if (LAYOUT_INFLATER_SERVICE.equals(name)) { 169 if (mInflater == null) { 170 mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); 171 } 172 return mInflater; 173 } 174 return getBaseContext().getSystemService(name); 175 } 176 177 /** 178 * Called by {@link #setTheme} and {@link #getTheme} to apply a theme 179 * resource to the current Theme object. Can override to change the 180 * default (simple) behavior. This method will not be called in multiple 181 * threads simultaneously. 182 * 183 * @param theme The Theme object being modified. 184 * @param resid The theme style resource being applied to <var>theme</var>. 185 * @param first Set to true if this is the first time a style is being 186 * applied to <var>theme</var>. 187 */ onApplyThemeResource(Resources.Theme theme, int resid, boolean first)188 protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { 189 theme.applyStyle(resid, true); 190 } 191 initializeTheme()192 private void initializeTheme() { 193 final boolean first = mTheme == null; 194 if (first) { 195 mTheme = getResources().newTheme(); 196 Resources.Theme theme = getBaseContext().getTheme(); 197 if (theme != null) { 198 mTheme.setTo(theme); 199 } 200 } 201 onApplyThemeResource(mTheme, mThemeResource, first); 202 } 203 204 @Override getAssets()205 public AssetManager getAssets() { 206 // Ensure we're returning assets with the correct configuration. 207 return getResources().getAssets(); 208 } 209 210 /** 211 * @return {@code true} if the specified configuration is {@code null} or is a no-op when 212 * used as a configuration overlay 213 */ 214 @RequiresApi(26) isEmptyConfiguration(Configuration overrideConfiguration)215 private static boolean isEmptyConfiguration(Configuration overrideConfiguration) { 216 if (overrideConfiguration == null) { 217 return true; 218 } 219 220 if (sEmptyConfig == null) { 221 Configuration emptyConfig = new Configuration(); 222 // Workaround for incorrect default fontScale on earlier SDKs (b/29924927). Note 223 // that Configuration.setToDefaults() is *not* a no-op configuration overlay. 224 emptyConfig.fontScale = 0.0f; 225 sEmptyConfig = emptyConfig; 226 } 227 228 return overrideConfiguration.equals(sEmptyConfig); 229 } 230 } 231 232